(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        define(['d3'], function (d3) {
            return (root.Rickshaw = factory(d3));
        });
    } else if (typeof exports === 'object') {
        module.exports = factory(require('d3'));
    } else {
        root.Rickshaw = factory(d3);
    }
}(this, function (d3) {
    /* jshint -W079 */

    var Rickshaw = {

        namespace: function (namespace, obj) {

            var parts = namespace.split('.');

            var parent = Rickshaw;

            for (var i = 1, length = parts.length; i < length; i++) {
                var currentPart = parts[i];
                parent[currentPart] = parent[currentPart] || {};
                parent = parent[currentPart];
            }
            return parent;
        },

        keys: function (obj) {
            var keys = [];
            for (var key in obj) keys.push(key);
            return keys;
        },

        extend: function (destination, source) {

            for (var property in source) {
                destination[property] = source[property];
            }
            return destination;
        },

        clone: function (obj) {
            return JSON.parse(JSON.stringify(obj));
        }
    };
    /* Adapted from https://github.com/Jakobo/PTClass */

    /*
     Copyright (c) 2005-2010 Sam Stephenson

     Permission is hereby granted, free of charge, to any person obtaining a copy
     of this software and associated documentation files (the "Software"), to deal
     in the Software without restriction, including without limitation the rights
     to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     copies of the Software, and to permit persons to whom the Software is
     furnished to do so, subject to the following conditions:

     THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
     IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
     FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
     AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
     LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
     OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
     SOFTWARE.
     */
    /* Based on Alex Arnell's inheritance implementation. */
    /** section: Language
     * class Class
     *
     *  Manages Prototype's class-based OOP system.
     *
     *  Refer to Prototype's web site for a [tutorial on classes and
     *  inheritance](http://prototypejs.org/learn/class-inheritance).
     **/
    (function (globalContext) {
        /* ------------------------------------ */
        /* Import from object.js                */
        /* ------------------------------------ */
        var _toString = Object.prototype.toString,
            NULL_TYPE = 'Null',
            UNDEFINED_TYPE = 'Undefined',
            BOOLEAN_TYPE = 'Boolean',
            NUMBER_TYPE = 'Number',
            STRING_TYPE = 'String',
            OBJECT_TYPE = 'Object',
            FUNCTION_CLASS = '[object Function]';

        function isFunction(object) {
            return _toString.call(object) === FUNCTION_CLASS;
        }

        function extend(destination, source) {
            for (var property in source) if (source.hasOwnProperty(property)) // modify protect primitive slaughter
                destination[property] = source[property];
            return destination;
        }

        function keys(object) {
            if (Type(object) !== OBJECT_TYPE) {
                throw new TypeError();
            }
            var results = [];
            for (var property in object) {
                if (object.hasOwnProperty(property)) {
                    results.push(property);
                }
            }
            return results;
        }

        function Type(o) {
            switch (o) {
                case null:
                    return NULL_TYPE;
                case (void 0):
                    return UNDEFINED_TYPE;
            }
            var type = typeof o;
            switch (type) {
                case 'boolean':
                    return BOOLEAN_TYPE;
                case 'number':
                    return NUMBER_TYPE;
                case 'string':
                    return STRING_TYPE;
            }
            return OBJECT_TYPE;
        }

        function isUndefined(object) {
            return typeof object === "undefined";
        }

        /* ------------------------------------ */
        /* Import from Function.js              */
        /* ------------------------------------ */
        var slice = Array.prototype.slice;

        function argumentNames(fn) {
            var names = fn.toString().match(/^[\s\(]*function[^(]*\(([^)]*)\)/)[1]
                .replace(/\/\/.*?[\r\n]|\/\*(?:.|[\r\n])*?\*\//g, '')
                .replace(/\s+/g, '').split(',');
            return names.length == 1 && !names[0] ? [] : names;
        }

        function wrap(fn, wrapper) {
            var __method = fn;
            return function () {
                var a = update([bind(__method, this)], arguments);
                return wrapper.apply(this, a);
            }
        }

        function update(array, args) {
            var arrayLength = array.length, length = args.length;
            while (length--) array[arrayLength + length] = args[length];
            return array;
        }

        function merge(array, args) {
            array = slice.call(array, 0);
            return update(array, args);
        }

        function bind(fn, context) {
            if (arguments.length < 2 && isUndefined(arguments[0])) return this;
            var __method = fn, args = slice.call(arguments, 2);
            return function () {
                var a = merge(args, arguments);
                return __method.apply(context, a);
            }
        }

        /* ------------------------------------ */
        /* Import from Prototype.js             */
        /* ------------------------------------ */
        var emptyFunction = function () {
        };

        var Class = (function () {

            // Some versions of JScript fail to enumerate over properties, names of which 
            // correspond to non-enumerable properties in the prototype chain
            var IS_DONTENUM_BUGGY = (function () {
                for (var p in {toString: 1}) {
                    // check actual property name, so that it works with augmented Object.prototype
                    if (p === 'toString') return false;
                }
                return true;
            })();

            function subclass() {
            };
            function create() {
                var parent = null, properties = [].slice.apply(arguments);
                if (isFunction(properties[0]))
                    parent = properties.shift();

                function klass() {
                    this.initialize.apply(this, arguments);
                }

                extend(klass, Class.Methods);
                klass.superclass = parent;
                klass.subclasses = [];

                if (parent) {
                    subclass.prototype = parent.prototype;
                    klass.prototype = new subclass;
                    try {
                        parent.subclasses.push(klass)
                    } catch (e) {
                    }
                }

                for (var i = 0, length = properties.length; i < length; i++)
                    klass.addMethods(properties[i]);

                if (!klass.prototype.initialize)
                    klass.prototype.initialize = emptyFunction;

                klass.prototype.constructor = klass;
                return klass;
            }

            function addMethods(source) {
                var ancestor = this.superclass && this.superclass.prototype,
                    properties = keys(source);

                // IE6 doesn't enumerate `toString` and `valueOf` (among other built-in `Object.prototype`) properties,
                // Force copy if they're not Object.prototype ones.
                // Do not copy other Object.prototype.* for performance reasons
                if (IS_DONTENUM_BUGGY) {
                    if (source.toString != Object.prototype.toString)
                        properties.push("toString");
                    if (source.valueOf != Object.prototype.valueOf)
                        properties.push("valueOf");
                }

                for (var i = 0, length = properties.length; i < length; i++) {
                    var property = properties[i], value = source[property];
                    if (ancestor && isFunction(value) &&
                        argumentNames(value)[0] == "$super") {
                        var method = value;
                        value = wrap((function (m) {
                            return function () {
                                return ancestor[m].apply(this, arguments);
                            };
                        })(property), method);

                        value.valueOf = bind(method.valueOf, method);
                        value.toString = bind(method.toString, method);
                    }
                    this.prototype[property] = value;
                }

                return this;
            }

            return {
                create: create,
                Methods: {
                    addMethods: addMethods
                }
            };
        })();

        if (globalContext.exports) {
            globalContext.exports.Class = Class;
        }
        else {
            globalContext.Class = Class;
        }
    })(Rickshaw);
    Rickshaw.namespace('Rickshaw.Compat.ClassList');

    Rickshaw.Compat.ClassList = function () {

        /* adapted from http://purl.eligrey.com/github/classList.js/blob/master/classList.js */

        if (typeof document !== "undefined" && !("classList" in document.createElement("a"))) {

            (function (view) {

                "use strict";

                var
                    classListProp = "classList"
                    , protoProp = "prototype"
                    , elemCtrProto = (view.HTMLElement || view.Element)[protoProp]
                    , objCtr = Object
                    , strTrim = String[protoProp].trim || function () {
                            return this.replace(/^\s+|\s+$/g, "");
                        }
                    , arrIndexOf = Array[protoProp].indexOf || function (item) {
                            var
                                i = 0
                                , len = this.length
                                ;
                            for (; i < len; i++) {
                                if (i in this && this[i] === item) {
                                    return i;
                                }
                            }
                            return -1;
                        }
                // Vendors: please allow content code to instantiate DOMExceptions
                    , DOMEx = function (type, message) {
                        this.name = type;
                        this.code = DOMException[type];
                        this.message = message;
                    }
                    , checkTokenAndGetIndex = function (classList, token) {
                        if (token === "") {
                            throw new DOMEx(
                                "SYNTAX_ERR"
                                , "An invalid or illegal string was specified"
                            );
                        }
                        if (/\s/.test(token)) {
                            throw new DOMEx(
                                "INVALID_CHARACTER_ERR"
                                , "String contains an invalid character"
                            );
                        }
                        return arrIndexOf.call(classList, token);
                    }
                    , ClassList = function (elem) {
                        var
                            trimmedClasses = strTrim.call(elem.className)
                            , classes = trimmedClasses ? trimmedClasses.split(/\s+/) : []
                            , i = 0
                            , len = classes.length
                            ;
                        for (; i < len; i++) {
                            this.push(classes[i]);
                        }
                        this._updateClassName = function () {
                            elem.className = this.toString();
                        };
                    }
                    , classListProto = ClassList[protoProp] = []
                    , classListGetter = function () {
                        return new ClassList(this);
                    }
                    ;
                // Most DOMException implementations don't allow calling DOMException's toString()
                // on non-DOMExceptions. Error's toString() is sufficient here.
                DOMEx[protoProp] = Error[protoProp];
                classListProto.item = function (i) {
                    return this[i] || null;
                };
                classListProto.contains = function (token) {
                    token += "";
                    return checkTokenAndGetIndex(this, token) !== -1;
                };
                classListProto.add = function (token) {
                    token += "";
                    if (checkTokenAndGetIndex(this, token) === -1) {
                        this.push(token);
                        this._updateClassName();
                    }
                };
                classListProto.remove = function (token) {
                    token += "";
                    var index = checkTokenAndGetIndex(this, token);
                    if (index !== -1) {
                        this.splice(index, 1);
                        this._updateClassName();
                    }
                };
                classListProto.toggle = function (token) {
                    token += "";
                    if (checkTokenAndGetIndex(this, token) === -1) {
                        this.add(token);
                    } else {
                        this.remove(token);
                    }
                };
                classListProto.toString = function () {
                    return this.join(" ");
                };

                if (objCtr.defineProperty) {
                    var classListPropDesc = {
                        get: classListGetter
                        , enumerable: true
                        , configurable: true
                    };
                    try {
                        objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc);
                    } catch (ex) { // IE 8 doesn't support enumerable:true
                        if (ex.number === -0x7FF5EC54) {
                            classListPropDesc.enumerable = false;
                            objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc);
                        }
                    }
                } else if (objCtr[protoProp].__defineGetter__) {
                    elemCtrProto.__defineGetter__(classListProp, classListGetter);
                }

            }(window));

        }
    };

    if ((typeof RICKSHAW_NO_COMPAT !== "undefined" && !RICKSHAW_NO_COMPAT) || typeof RICKSHAW_NO_COMPAT === "undefined") {
        new Rickshaw.Compat.ClassList();
    }
    Rickshaw.namespace('Rickshaw.Graph');

    Rickshaw.Graph = function (args) {

        var self = this;

        this.initialize = function (args) {

            if (!args.element) throw "Rickshaw.Graph needs a reference to an element";
            if (args.element.nodeType !== 1) throw "Rickshaw.Graph element was defined but not an HTML element";

            this.element = args.element;
            this.series = args.series;
            this.window = {};

            this.updateCallbacks = [];
            this.configureCallbacks = [];

            this.defaults = {
                interpolation: 'cardinal',
                offset: 'zero',
                min: undefined,
                max: undefined,
                preserve: false,
                xScale: undefined,
                yScale: undefined,
                stack: true
            };

            this._loadRenderers();
            this.configure(args);
            this.validateSeries(args.series);

            this.series.active = function () {
                return self.series.filter(function (s) {
                    return !s.disabled
                })
            };
            this.setSize({width: args.width, height: args.height});
            this.element.classList.add('rickshaw_graph');

            this.vis = d3.select(this.element)
                .append("svg:svg")
                .attr('width', this.width)
                .attr('height', this.height);

            this.discoverRange();
        };

        this._loadRenderers = function () {

            for (var name in Rickshaw.Graph.Renderer) {
                if (!name || !Rickshaw.Graph.Renderer.hasOwnProperty(name)) continue;
                var r = Rickshaw.Graph.Renderer[name];
                if (!r || !r.prototype || !r.prototype.render) continue;
                self.registerRenderer(new r({graph: self}));
            }
        };

        this.validateSeries = function (series) {

            if (!Array.isArray(series) && !(series instanceof Rickshaw.Series)) {
                var seriesSignature = Object.prototype.toString.apply(series);
                throw "series is not an array: " + seriesSignature;
            }

            var pointsCount;

            series.forEach(function (s) {

                if (!(s instanceof Object)) {
                    throw "series element is not an object: " + s;
                }
                if (!(s.data)) {
                    throw "series has no data: " + JSON.stringify(s);
                }
                if (!Array.isArray(s.data)) {
                    throw "series data is not an array: " + JSON.stringify(s.data);
                }

                if (s.data.length > 0) {
                    var x = s.data[0].x;
                    var y = s.data[0].y;

                    if (typeof x != 'number' || ( typeof y != 'number' && y !== null )) {
                        throw "x and y properties of points should be numbers instead of " +
                        (typeof x) + " and " + (typeof y);
                    }
                }

                if (s.data.length >= 3) {
                    // probe to sanity check sort order
                    if (s.data[2].x < s.data[1].x || s.data[1].x < s.data[0].x || s.data[s.data.length - 1].x < s.data[0].x) {
                        throw "series data needs to be sorted on x values for series name: " + s.name;
                    }
                }

            }, this);
        };

        this.dataDomain = function () {

            var data = this.series.map(function (s) {
                return s.data
            });

            var min = d3.min(data.map(function (d) {
                return d[0].x
            }));
            var max = d3.max(data.map(function (d) {
                return d[d.length - 1].x
            }));

            return [min, max];
        };

        this.discoverRange = function () {

            var domain = this.renderer.domain();

            // this.*Scale is coming from the configuration dictionary
            // which may be referenced by the Graph creator, or shared
            // with other Graphs. We need to ensure we copy the scale
            // so that our mutations do not change the object given to us.
            // Hence the .copy()
            this.x = (this.xScale || d3.scale.linear()).copy().domain(domain.x).range([0, this.width]);
            this.y = (this.yScale || d3.scale.linear()).copy().domain(domain.y).range([this.height, 0]);

            this.x.magnitude = d3.scale.linear()
                .domain([domain.x[0] - domain.x[0], domain.x[1] - domain.x[0]])
                .range([0, this.width]);

            this.y.magnitude = d3.scale.linear()
                .domain([domain.y[0] - domain.y[0], domain.y[1] - domain.y[0]])
                .range([0, this.height]);
        };

        this.render = function () {

            var stackedData = this.stackData();
            this.discoverRange();

            this.renderer.render();

            this.updateCallbacks.forEach(function (callback) {
                callback();
            });

        };

        this.update = this.render;

        this.stackData = function () {

            var data = this.series.active()
                .map(function (d) {
                    return d.data
                })
                .map(function (d) {
                    return d.filter(function (d) {
                        return this._slice(d)
                    }, this)
                }, this);

            var preserve = this.preserve;
            if (!preserve) {
                this.series.forEach(function (series) {
                    if (series.scale) {
                        // data must be preserved when a scale is used
                        preserve = true;
                    }
                });
            }

            data = preserve ? Rickshaw.clone(data) : data;

            this.series.active().forEach(function (series, index) {
                if (series.scale) {
                    // apply scale to each series
                    var seriesData = data[index];
                    if (seriesData) {
                        seriesData.forEach(function (d) {
                            d.y = series.scale(d.y);
                        });
                    }
                }
            });

            this.stackData.hooks.data.forEach(function (entry) {
                data = entry.f.apply(self, [data]);
            });

            var stackedData;

            if (!this.renderer.unstack) {

                this._validateStackable();

                var layout = d3.layout.stack();
                layout.offset(self.offset);
                stackedData = layout(data);
            }

            stackedData = stackedData || data;

            if (this.renderer.unstack) {
                stackedData.forEach(function (seriesData) {
                    seriesData.forEach(function (d) {
                        d.y0 = d.y0 === undefined ? 0 : d.y0;
                    });
                });
            }

            this.stackData.hooks.after.forEach(function (entry) {
                stackedData = entry.f.apply(self, [data]);
            });

            var i = 0;
            this.series.forEach(function (series) {
                if (series.disabled) return;
                series.stack = stackedData[i++];
            });

            this.stackedData = stackedData;
            return stackedData;
        };

        this._validateStackable = function () {

            var series = this.series;
            var pointsCount;

            series.forEach(function (s) {

                pointsCount = pointsCount || s.data.length;

                if (pointsCount && s.data.length != pointsCount) {
                    throw "stacked series cannot have differing numbers of points: " +
                    pointsCount + " vs " + s.data.length + "; see Rickshaw.Series.fill()";
                }

            }, this);
        };

        this.stackData.hooks = {data: [], after: []};

        this._slice = function (d) {

            if (this.window.xMin || this.window.xMax) {

                var isInRange = true;

                if (this.window.xMin && d.x < this.window.xMin) isInRange = false;
                if (this.window.xMax && d.x > this.window.xMax) isInRange = false;

                return isInRange;
            }

            return true;
        };

        this.onUpdate = function (callback) {
            this.updateCallbacks.push(callback);
        };

        this.onConfigure = function (callback) {
            this.configureCallbacks.push(callback);
        };

        this.registerRenderer = function (renderer) {
            this._renderers = this._renderers || {};
            this._renderers[renderer.name] = renderer;
        };

        this.configure = function (args) {

            this.config = this.config || {};

            if (args.width || args.height) {
                this.setSize(args);
            }

            Rickshaw.keys(this.defaults).forEach(function (k) {
                this.config[k] = k in args ? args[k]
                    : k in this ? this[k]
                    : this.defaults[k];
            }, this);

            Rickshaw.keys(this.config).forEach(function (k) {
                this[k] = this.config[k];
            }, this);

            if ('stack' in args) args.unstack = !args.stack;

            var renderer = args.renderer || (this.renderer && this.renderer.name) || 'stack';
            this.setRenderer(renderer, args);

            this.configureCallbacks.forEach(function (callback) {
                callback(args);
            });
        };

        this.setRenderer = function (r, args) {
            if (typeof r == 'function') {
                this.renderer = new r({graph: self});
                this.registerRenderer(this.renderer);
            } else {
                if (!this._renderers[r]) {
                    throw "couldn't find renderer " + r;
                }
                this.renderer = this._renderers[r];
            }

            if (typeof args == 'object') {
                this.renderer.configure(args);
            }
        };

        this.setSize = function (args) {

            args = args || {};

            if (typeof window !== undefined) {
                var style = window.getComputedStyle(this.element, null);
                var elementWidth = parseInt(style.getPropertyValue('width'), 10);
                var elementHeight = parseInt(style.getPropertyValue('height'), 10);
            }

            this.width = args.width || elementWidth || 400;
            this.height = args.height || elementHeight || 250;

            this.vis && this.vis
                .attr('width', this.width)
                .attr('height', this.height);
        };

        this.initialize(args);
    };
    Rickshaw.namespace('Rickshaw.Fixtures.RandomData');

    Rickshaw.Fixtures.RandomData = function (timeInterval) {

        var addData;
        timeInterval = timeInterval || 1;

        var lastRandomValue = 200;

        var timeBase = Math.floor(new Date().getTime() / 1000);

        this.addData = function (data) {

            var randomValue = Math.random() * 100 + 15 + lastRandomValue;
            var index = data[0].length;

            var counter = 1;

            data.forEach(function (series) {
                var randomVariance = Math.random() * 20;
                var v = randomValue / 25 + counter++ +
                    (Math.cos((index * counter * 11) / 960) + 2) * 15 +
                    (Math.cos(index / 7) + 2) * 7 +
                    (Math.cos(index / 17) + 2) * 1;

                series.push({x: (index * timeInterval) + timeBase, y: v + randomVariance});
            });

            lastRandomValue = randomValue * 0.85;
        };

        this.removeData = function (data) {
            data.forEach(function (series) {
                series.shift();
            });
            timeBase += timeInterval;
        };
    };

    Rickshaw.namespace('Rickshaw.Fixtures.Time');

    Rickshaw.Fixtures.Time = function () {

        var self = this;

        this.months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];

        this.units = [
            {
                name: 'decade',
                seconds: 86400 * 365.25 * 10,
                formatter: function (d) {
                    return (parseInt(d.getUTCFullYear() / 10, 10) * 10)
                }
            }, {
                name: 'year',
                seconds: 86400 * 365.25,
                formatter: function (d) {
                    return d.getUTCFullYear()
                }
            }, {
                name: 'month',
                seconds: 86400 * 30.5,
                formatter: function (d) {
                    return self.months[d.getUTCMonth()]
                }
            }, {
                name: 'week',
                seconds: 86400 * 7,
                formatter: function (d) {
                    return self.formatDate(d)
                }
            }, {
                name: 'day',
                seconds: 86400,
                formatter: function (d) {
                    return d.getUTCDate()
                }
            }, {
                name: '6 hour',
                seconds: 3600 * 6,
                formatter: function (d) {
                    return self.formatTime(d)
                }
            }, {
                name: 'hour',
                seconds: 3600,
                formatter: function (d) {
                    return self.formatTime(d)
                }
            }, {
                name: '15 minute',
                seconds: 60 * 15,
                formatter: function (d) {
                    return self.formatTime(d)
                }
            }, {
                name: 'minute',
                seconds: 60,
                formatter: function (d) {
                    return d.getUTCMinutes()
                }
            }, {
                name: '15 second',
                seconds: 15,
                formatter: function (d) {
                    return d.getUTCSeconds() + 's'
                }
            }, {
                name: 'second',
                seconds: 1,
                formatter: function (d) {
                    return d.getUTCSeconds() + 's'
                }
            }, {
                name: 'decisecond',
                seconds: 1 / 10,
                formatter: function (d) {
                    return d.getUTCMilliseconds() + 'ms'
                }
            }, {
                name: 'centisecond',
                seconds: 1 / 100,
                formatter: function (d) {
                    return d.getUTCMilliseconds() + 'ms'
                }
            }
        ];

        this.unit = function (unitName) {
            return this.units.filter(function (unit) {
                return unitName == unit.name
            }).shift();
        };

        this.formatDate = function (d) {
            return d3.time.format('%b %e')(d);
        };

        this.formatTime = function (d) {
            return d.toUTCString().match(/(\d+:\d+):/)[1];
        };

        this.ceil = function (time, unit) {

            var date, floor, year;

            if (unit.name == 'month') {

                date = new Date(time * 1000);

                floor = Date.UTC(date.getUTCFullYear(), date.getUTCMonth()) / 1000;
                if (floor == time) return time;

                year = date.getUTCFullYear();
                var month = date.getUTCMonth();

                if (month == 11) {
                    month = 0;
                    year = year + 1;
                } else {
                    month += 1;
                }

                return Date.UTC(year, month) / 1000;
            }

            if (unit.name == 'year') {

                date = new Date(time * 1000);

                floor = Date.UTC(date.getUTCFullYear(), 0) / 1000;
                if (floor == time) return time;

                year = date.getUTCFullYear() + 1;

                return Date.UTC(year, 0) / 1000;
            }

            return Math.ceil(time / unit.seconds) * unit.seconds;
        };
    };
    Rickshaw.namespace('Rickshaw.Fixtures.Time.Local');

    Rickshaw.Fixtures.Time.Local = function () {

        var self = this;

        this.months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];

        this.units = [
            {
                name: 'decade',
                seconds: 86400 * 365.25 * 10,
                formatter: function (d) {
                    return (parseInt(d.getFullYear() / 10, 10) * 10)
                }
            }, {
                name: 'year',
                seconds: 86400 * 365.25,
                formatter: function (d) {
                    return d.getFullYear()
                }
            }, {
                name: 'month',
                seconds: 86400 * 30.5,
                formatter: function (d) {
                    return self.months[d.getMonth()]
                }
            }, {
                name: 'week',
                seconds: 86400 * 7,
                formatter: function (d) {
                    return self.formatDate(d)
                }
            }, {
                name: 'day',
                seconds: 86400,
                formatter: function (d) {
                    return d.getDate()
                }
            }, {
                name: '6 hour',
                seconds: 3600 * 6,
                formatter: function (d) {
                    return self.formatTime(d)
                }
            }, {
                name: 'hour',
                seconds: 3600,
                formatter: function (d) {
                    return self.formatTime(d)
                }
            }, {
                name: '15 minute',
                seconds: 60 * 15,
                formatter: function (d) {
                    return self.formatTime(d)
                }
            }, {
                name: 'minute',
                seconds: 60,
                formatter: function (d) {
                    return d.getMinutes()
                }
            }, {
                name: '15 second',
                seconds: 15,
                formatter: function (d) {
                    return d.getSeconds() + 's'
                }
            }, {
                name: 'second',
                seconds: 1,
                formatter: function (d) {
                    return d.getSeconds() + 's'
                }
            }, {
                name: 'decisecond',
                seconds: 1 / 10,
                formatter: function (d) {
                    return d.getMilliseconds() + 'ms'
                }
            }, {
                name: 'centisecond',
                seconds: 1 / 100,
                formatter: function (d) {
                    return d.getMilliseconds() + 'ms'
                }
            }
        ];

        this.unit = function (unitName) {
            return this.units.filter(function (unit) {
                return unitName == unit.name
            }).shift();
        };

        this.formatDate = function (d) {
            return d3.time.format('%b %e')(d);
        };

        this.formatTime = function (d) {
            return d.toString().match(/(\d+:\d+):/)[1];
        };

        this.ceil = function (time, unit) {

            var date, floor, year;

            if (unit.name == 'day') {

                var nearFuture = new Date((time + unit.seconds - 1) * 1000);

                var rounded = new Date(0);
                rounded.setMilliseconds(0);
                rounded.setSeconds(0);
                rounded.setMinutes(0);
                rounded.setHours(0);
                rounded.setDate(nearFuture.getDate());
                rounded.setMonth(nearFuture.getMonth());
                rounded.setFullYear(nearFuture.getFullYear());

                return rounded.getTime() / 1000;
            }

            if (unit.name == 'month') {

                date = new Date(time * 1000);

                floor = new Date(date.getFullYear(), date.getMonth()).getTime() / 1000;
                if (floor == time) return time;

                year = date.getFullYear();
                var month = date.getMonth();

                if (month == 11) {
                    month = 0;
                    year = year + 1;
                } else {
                    month += 1;
                }

                return new Date(year, month).getTime() / 1000;
            }

            if (unit.name == 'year') {

                date = new Date(time * 1000);

                floor = new Date(date.getUTCFullYear(), 0).getTime() / 1000;
                if (floor == time) return time;

                year = date.getFullYear() + 1;

                return new Date(year, 0).getTime() / 1000;
            }

            return Math.ceil(time / unit.seconds) * unit.seconds;
        };
    };
    Rickshaw.namespace('Rickshaw.Fixtures.Number');

    Rickshaw.Fixtures.Number.formatKMBT = function (y) {
        var abs_y = Math.abs(y);
        if (abs_y >= 1000000000000) {
            return y / 1000000000000 + "T"
        }
        else if (abs_y >= 1000000000) {
            return y / 1000000000 + "B"
        }
        else if (abs_y >= 1000000) {
            return y / 1000000 + "M"
        }
        else if (abs_y >= 1000) {
            return y / 1000 + "K"
        }
        else if (abs_y < 1 && y > 0) {
            return y.toFixed(2)
        }
        else if (abs_y === 0) {
            return ''
        }
        else {
            return y
        }
    };

    Rickshaw.Fixtures.Number.formatBase1024KMGTP = function (y) {
        var abs_y = Math.abs(y);
        if (abs_y >= 1125899906842624) {
            return y / 1125899906842624 + "P"
        }
        else if (abs_y >= 1099511627776) {
            return y / 1099511627776 + "T"
        }
        else if (abs_y >= 1073741824) {
            return y / 1073741824 + "G"
        }
        else if (abs_y >= 1048576) {
            return y / 1048576 + "M"
        }
        else if (abs_y >= 1024) {
            return y / 1024 + "K"
        }
        else if (abs_y < 1 && y > 0) {
            return y.toFixed(2)
        }
        else if (abs_y === 0) {
            return ''
        }
        else {
            return y
        }
    };
    Rickshaw.namespace("Rickshaw.Color.Palette");

    Rickshaw.Color.Palette = function (args) {

        var color = new Rickshaw.Fixtures.Color();

        args = args || {};
        this.schemes = {};

        this.scheme = color.schemes[args.scheme] || args.scheme || color.schemes.colorwheel;
        this.runningIndex = 0;
        this.generatorIndex = 0;

        if (args.interpolatedStopCount) {
            var schemeCount = this.scheme.length - 1;
            var i, j, scheme = [];
            for (i = 0; i < schemeCount; i++) {
                scheme.push(this.scheme[i]);
                var generator = d3.interpolateHsl(this.scheme[i], this.scheme[i + 1]);
                for (j = 1; j < args.interpolatedStopCount; j++) {
                    scheme.push(generator((1 / args.interpolatedStopCount) * j));
                }
            }
            scheme.push(this.scheme[this.scheme.length - 1]);
            this.scheme = scheme;
        }
        this.rotateCount = this.scheme.length;

        this.color = function (key) {
            return this.scheme[key] || this.scheme[this.runningIndex++] || this.interpolateColor() || '#808080';
        };

        this.interpolateColor = function () {
            if (!Array.isArray(this.scheme)) return;
            var color;
            if (this.generatorIndex == this.rotateCount * 2 - 1) {
                color = d3.interpolateHsl(this.scheme[this.generatorIndex], this.scheme[0])(0.5);
                this.generatorIndex = 0;
                this.rotateCount *= 2;
            } else {
                color = d3.interpolateHsl(this.scheme[this.generatorIndex], this.scheme[this.generatorIndex + 1])(0.5);
                this.generatorIndex++;
            }
            this.scheme.push(color);
            return color;
        };

    };
    Rickshaw.namespace('Rickshaw.Graph.Ajax');

    Rickshaw.Graph.Ajax = Rickshaw.Class.create({

        initialize: function (args) {

            this.dataURL = args.dataURL;

            this.onData = args.onData || function (d) {
                    return d
                };
            this.onComplete = args.onComplete || function () {
                };
            this.onError = args.onError || function () {
                };

            this.args = args; // pass through to Rickshaw.Graph

            this.request();
        },

        request: function () {

            jQuery.ajax({
                url: this.dataURL,
                dataType: 'json',
                success: this.success.bind(this),
                error: this.error.bind(this)
            });
        },

        error: function () {

            console.log("error loading dataURL: " + this.dataURL);
            this.onError(this);
        },

        success: function (data, status) {

            data = this.onData(data);
            this.args.series = this._splice({data: data, series: this.args.series});

            this.graph = this.graph || new Rickshaw.Graph(this.args);
            this.graph.render();

            this.onComplete(this);
        },

        _splice: function (args) {

            var data = args.data;
            var series = args.series;

            if (!args.series) return data;

            series.forEach(function (s) {

                var seriesKey = s.key || s.name;
                if (!seriesKey) throw "series needs a key or a name";

                data.forEach(function (d) {

                    var dataKey = d.key || d.name;
                    if (!dataKey) throw "data needs a key or a name";

                    if (seriesKey == dataKey) {
                        var properties = ['color', 'name', 'data'];
                        properties.forEach(function (p) {
                            if (d[p]) s[p] = d[p];
                        });
                    }
                });
            });

            return series;
        }
    });

    Rickshaw.namespace('Rickshaw.Graph.Annotate');

    Rickshaw.Graph.Annotate = function (args) {

        var graph = this.graph = args.graph;
        this.elements = {timeline: args.element};

        var self = this;

        this.data = {};

        this.elements.timeline.classList.add('rickshaw_annotation_timeline');

        this.add = function (time, content, end_time) {
            self.data[time] = self.data[time] || {'boxes': []};
            self.data[time].boxes.push({content: content, end: end_time});
        };

        this.update = function () {

            Rickshaw.keys(self.data).forEach(function (time) {

                var annotation = self.data[time];
                var left = self.graph.x(time);

                if (left < 0 || left > self.graph.x.range()[1]) {
                    if (annotation.element) {
                        annotation.line.classList.add('offscreen');
                        annotation.element.style.display = 'none';
                    }

                    annotation.boxes.forEach(function (box) {
                        if (box.rangeElement) box.rangeElement.classList.add('offscreen');
                    });

                    return;
                }

                if (!annotation.element) {
                    var element = annotation.element = document.createElement('div');
                    element.classList.add('annotation');
                    this.elements.timeline.appendChild(element);
                    element.addEventListener('click', function (e) {
                        element.classList.toggle('active');
                        annotation.line.classList.toggle('active');
                        annotation.boxes.forEach(function (box) {
                            if (box.rangeElement) box.rangeElement.classList.toggle('active');
                        });
                    }, false);

                }

                annotation.element.style.left = left + 'px';
                annotation.element.style.display = 'block';

                annotation.boxes.forEach(function (box) {


                    var element = box.element;

                    if (!element) {
                        element = box.element = document.createElement('div');
                        element.classList.add('content');
                        element.innerHTML = box.content;
                        annotation.element.appendChild(element);

                        annotation.line = document.createElement('div');
                        annotation.line.classList.add('annotation_line');
                        self.graph.element.appendChild(annotation.line);

                        if (box.end) {
                            box.rangeElement = document.createElement('div');
                            box.rangeElement.classList.add('annotation_range');
                            self.graph.element.appendChild(box.rangeElement);
                        }

                    }

                    if (box.end) {

                        var annotationRangeStart = left;
                        var annotationRangeEnd = Math.min(self.graph.x(box.end), self.graph.x.range()[1]);

                        // annotation makes more sense at end
                        if (annotationRangeStart > annotationRangeEnd) {
                            annotationRangeEnd = left;
                            annotationRangeStart = Math.max(self.graph.x(box.end), self.graph.x.range()[0]);
                        }

                        var annotationRangeWidth = annotationRangeEnd - annotationRangeStart;

                        box.rangeElement.style.left = annotationRangeStart + 'px';
                        box.rangeElement.style.width = annotationRangeWidth + 'px';

                        box.rangeElement.classList.remove('offscreen');
                    }

                    annotation.line.classList.remove('offscreen');
                    annotation.line.style.left = left + 'px';
                });
            }, this);
        };

        this.graph.onUpdate(function () {
            self.update()
        });
    };
    Rickshaw.namespace('Rickshaw.Graph.Axis.Time');

    Rickshaw.Graph.Axis.Time = function (args) {

        var self = this;

        this.graph = args.graph;
        this.elements = [];
        this.ticksTreatment = args.ticksTreatment || 'plain';
        this.fixedTimeUnit = args.timeUnit;

        var time = args.timeFixture || new Rickshaw.Fixtures.Time();

        this.appropriateTimeUnit = function () {

            var unit;
            var units = time.units;

            var domain = this.graph.x.domain();
            var rangeSeconds = domain[1] - domain[0];

            units.forEach(function (u) {
                if (Math.floor(rangeSeconds / u.seconds) >= 2) {
                    unit = unit || u;
                }
            });

            return (unit || time.units[time.units.length - 1]);
        };

        this.tickOffsets = function () {

            var domain = this.graph.x.domain();

            var unit = this.fixedTimeUnit || this.appropriateTimeUnit();
            var count = Math.ceil((domain[1] - domain[0]) / unit.seconds);

            var runningTick = domain[0];

            var offsets = [];

            for (var i = 0; i < count; i++) {

                var tickValue = time.ceil(runningTick, unit);
                runningTick = tickValue + unit.seconds / 2;

                offsets.push({value: tickValue, unit: unit});
            }

            return offsets;
        };

        this.render = function () {

            this.elements.forEach(function (e) {
                e.parentNode.removeChild(e);
            });

            this.elements = [];

            var offsets = this.tickOffsets();

            offsets.forEach(function (o) {

                if (self.graph.x(o.value) > self.graph.x.range()[1]) return;

                var element = document.createElement('div');
                element.style.left = self.graph.x(o.value) + 'px';
                element.classList.add('x_tick');
                element.classList.add(self.ticksTreatment);

                var title = document.createElement('div');
                title.classList.add('title');
                title.innerHTML = o.unit.formatter(new Date(o.value * 1000));
                element.appendChild(title);

                self.graph.element.appendChild(element);
                self.elements.push(element);

            });
        };

        this.graph.onUpdate(function () {
            self.render()
        });
    };

    Rickshaw.namespace('Rickshaw.Graph.Axis.X');

    Rickshaw.Graph.Axis.X = function (args) {

        var self = this;
        var berthRate = 0.10;

        this.initialize = function (args) {

            this.graph = args.graph;
            this.orientation = args.orientation || 'top';

            this.pixelsPerTick = args.pixelsPerTick || 75;
            if (args.ticks) this.staticTicks = args.ticks;
            if (args.tickValues) this.tickValues = args.tickValues;

            this.tickSize = args.tickSize || 4;
            this.ticksTreatment = args.ticksTreatment || 'plain';

            if (args.element) {

                this.element = args.element;
                this._discoverSize(args.element, args);

                this.vis = d3.select(args.element)
                    .append("svg:svg")
                    .attr('height', this.height)
                    .attr('width', this.width)
                    .attr('class', 'rickshaw_graph x_axis_d3');

                this.element = this.vis[0][0];
                this.element.style.position = 'relative';

                this.setSize({width: args.width, height: args.height});

            } else {
                this.vis = this.graph.vis;
            }

            this.graph.onUpdate(function () {
                self.render()
            });
        };

        this.setSize = function (args) {

            args = args || {};
            if (!this.element) return;

            this._discoverSize(this.element.parentNode, args);

            this.vis
                .attr('height', this.height)
                .attr('width', this.width * (1 + berthRate));

            var berth = Math.floor(this.width * berthRate / 2);
            this.element.style.left = -1 * berth + 'px';
        };

        this.render = function () {

            if (this._renderWidth !== undefined && this.graph.width !== this._renderWidth) this.setSize({auto: true});

            var axis = d3.svg.axis().scale(this.graph.x).orient(this.orientation);
            axis.tickFormat(args.tickFormat || function (x) {
                    return x
                });
            if (this.tickValues) axis.tickValues(this.tickValues);

            this.ticks = this.staticTicks || Math.floor(this.graph.width / this.pixelsPerTick);

            var berth = Math.floor(this.width * berthRate / 2) || 0;
            var transform;

            if (this.orientation == 'top') {
                var yOffset = this.height || this.graph.height;
                transform = 'translate(' + berth + ',' + yOffset + ')';
            } else {
                transform = 'translate(' + berth + ', 0)';
            }

            if (this.element) {
                this.vis.selectAll('*').remove();
            }

            this.vis
                .append("svg:g")
                .attr("class", ["x_ticks_d3", this.ticksTreatment].join(" "))
                .attr("transform", transform)
                .call(axis.ticks(this.ticks).tickSubdivide(0).tickSize(this.tickSize));

            var gridSize = (this.orientation == 'bottom' ? 1 : -1) * this.graph.height;

            this.graph.vis
                .append("svg:g")
                .attr("class", "x_grid_d3")
                .call(axis.ticks(this.ticks).tickSubdivide(0).tickSize(gridSize))
                .selectAll('text')
                .each(function () {
                    this.parentNode.setAttribute('data-x-value', this.textContent)
                });

            this._renderHeight = this.graph.height;
        };

        this._discoverSize = function (element, args) {

            if (typeof window !== 'undefined') {

                var style = window.getComputedStyle(element, null);
                var elementHeight = parseInt(style.getPropertyValue('height'), 10);

                if (!args.auto) {
                    var elementWidth = parseInt(style.getPropertyValue('width'), 10);
                }
            }

            this.width = (args.width || elementWidth || this.graph.width) * (1 + berthRate);
            this.height = args.height || elementHeight || 40;
        };

        this.initialize(args);
    };

    Rickshaw.namespace('Rickshaw.Graph.Axis.Y');

    Rickshaw.Graph.Axis.Y = Rickshaw.Class.create({

        initialize: function (args) {

            this.graph = args.graph;
            this.orientation = args.orientation || 'right';

            this.pixelsPerTick = args.pixelsPerTick || 75;
            if (args.ticks) this.staticTicks = args.ticks;
            if (args.tickValues) this.tickValues = args.tickValues;

            this.tickSize = args.tickSize || 4;
            this.ticksTreatment = args.ticksTreatment || 'plain';

            this.tickFormat = args.tickFormat || function (y) {
                    return y
                };

            this.berthRate = 0.10;

            if (args.element) {

                this.element = args.element;
                this.vis = d3.select(args.element)
                    .append("svg:svg")
                    .attr('class', 'rickshaw_graph y_axis');

                this.element = this.vis[0][0];
                this.element.style.position = 'relative';

                this.setSize({width: args.width, height: args.height});

            } else {
                this.vis = this.graph.vis;
            }

            var self = this;
            this.graph.onUpdate(function () {
                self.render()
            });
        },

        setSize: function (args) {

            args = args || {};

            if (!this.element) return;

            if (typeof window !== 'undefined') {

                var style = window.getComputedStyle(this.element.parentNode, null);
                var elementWidth = parseInt(style.getPropertyValue('width'), 10);

                if (!args.auto) {
                    var elementHeight = parseInt(style.getPropertyValue('height'), 10);
                }
            }

            this.width = args.width || elementWidth || this.graph.width * this.berthRate;
            this.height = args.height || elementHeight || this.graph.height;

            this.vis
                .attr('width', this.width)
                .attr('height', this.height * (1 + this.berthRate));

            var berth = this.height * this.berthRate;

            if (this.orientation == 'left') {
                this.element.style.top = -1 * berth + 'px';
            }
        },

        render: function () {

            if (this._renderHeight !== undefined && this.graph.height !== this._renderHeight) this.setSize({auto: true});

            this.ticks = this.staticTicks || Math.floor(this.graph.height / this.pixelsPerTick);

            var axis = this._drawAxis(this.graph.y);

            this._drawGrid(axis);

            this._renderHeight = this.graph.height;
        },

        _drawAxis: function (scale) {
            var axis = d3.svg.axis().scale(scale).orient(this.orientation);
            axis.tickFormat(this.tickFormat);
            if (this.tickValues) axis.tickValues(this.tickValues);

            if (this.orientation == 'left') {
                var berth = this.height * this.berthRate;
                var transform = 'translate(' + this.width + ', ' + berth + ')';
            }

            if (this.element) {
                this.vis.selectAll('*').remove();
            }

            this.vis
                .append("svg:g")
                .attr("class", ["y_ticks", this.ticksTreatment].join(" "))
                .attr("transform", transform)
                .call(axis.ticks(this.ticks).tickSubdivide(0).tickSize(this.tickSize));

            return axis;
        },

        _drawGrid: function (axis) {
            var gridSize = (this.orientation == 'right' ? 1 : -1) * this.graph.width;

            this.graph.vis
                .append("svg:g")
                .attr("class", "y_grid")
                .call(axis.ticks(this.ticks).tickSubdivide(0).tickSize(gridSize))
                .selectAll('text')
                .each(function () {
                    this.parentNode.setAttribute('data-y-value', this.textContent)
                });
        }
    });
    Rickshaw.namespace('Rickshaw.Graph.Axis.Y.Scaled');

    Rickshaw.Graph.Axis.Y.Scaled = Rickshaw.Class.create(Rickshaw.Graph.Axis.Y, {

        initialize: function ($super, args) {

            if (typeof(args.scale) === 'undefined') {
                throw new Error('Scaled requires scale');
            }

            this.scale = args.scale;

            if (typeof(args.grid) === 'undefined') {
                this.grid = true;
            } else {
                this.grid = args.grid;
            }

            $super(args);

        },

        _drawAxis: function ($super, scale) {
            // Adjust scale's domain to compensate for adjustments to the
            // renderer's domain (e.g. padding).
            var domain = this.scale.domain();
            var renderDomain = this.graph.renderer.domain().y;

            var extents = [
                Math.min.apply(Math, domain),
                Math.max.apply(Math, domain)];

            // A mapping from the ideal render domain [0, 1] to the extent
            // of the original scale's domain.  This is used to calculate
            // the extents of the adjusted domain.
            var extentMap = d3.scale.linear().domain([0, 1]).range(extents);

            var adjExtents = [
                extentMap(renderDomain[0]),
                extentMap(renderDomain[1])];

            // A mapping from the original domain to the adjusted domain.
            var adjustment = d3.scale.linear().domain(extents).range(adjExtents);

            // Make a copy of the custom scale, apply the adjusted domain, and
            // copy the range to match the graph's scale.
            var adjustedScale = this.scale.copy()
                .domain(domain.map(adjustment))
                .range(scale.range());

            return $super(adjustedScale);
        },

        _drawGrid: function ($super, axis) {
            if (this.grid) {
                // only draw the axis if the grid option is true
                $super(axis);
            }
        }
    });
    Rickshaw.namespace('Rickshaw.Graph.Behavior.Series.Highlight');

    Rickshaw.Graph.Behavior.Series.Highlight = function (args) {

        this.graph = args.graph;
        this.legend = args.legend;

        var self = this;

        var colorSafe = {};
        var activeLine = null;

        var disabledColor = args.disabledColor || function (seriesColor) {
                return d3.interpolateRgb(seriesColor, d3.rgb('#d8d8d8'))(0.8).toString();
            };

        this.addHighlightEvents = function (l) {

            l.element.addEventListener('mouseover', function (e) {

                if (activeLine) return;
                else activeLine = l;

                self.legend.lines.forEach(function (line) {

                    if (l === line) {

                        // if we're not in a stacked renderer bring active line to the top
                        if (self.graph.renderer.unstack && (line.series.renderer ? line.series.renderer.unstack : true)) {

                            var seriesIndex = self.graph.series.indexOf(line.series);
                            line.originalIndex = seriesIndex;

                            var series = self.graph.series.splice(seriesIndex, 1)[0];
                            self.graph.series.push(series);
                        }
                        return;
                    }

                    colorSafe[line.series.name] = colorSafe[line.series.name] || line.series.color;
                    line.series.color = disabledColor(line.series.color);

                });

                self.graph.update();

            }, false);

            l.element.addEventListener('mouseout', function (e) {

                if (!activeLine) return;
                else activeLine = null;

                self.legend.lines.forEach(function (line) {

                    // return reordered series to its original place
                    if (l === line && line.hasOwnProperty('originalIndex')) {

                        var series = self.graph.series.pop();
                        self.graph.series.splice(line.originalIndex, 0, series);
                        delete line.originalIndex;
                    }

                    if (colorSafe[line.series.name]) {
                        line.series.color = colorSafe[line.series.name];
                    }
                });

                self.graph.update();

            }, false);
        };

        if (this.legend) {
            this.legend.lines.forEach(function (l) {
                self.addHighlightEvents(l);
            });
        }

    };
    Rickshaw.namespace('Rickshaw.Graph.Behavior.Series.Order');

    Rickshaw.Graph.Behavior.Series.Order = function (args) {

        this.graph = args.graph;
        this.legend = args.legend;

        var self = this;

        if (typeof window.jQuery == 'undefined') {
            throw "couldn't find jQuery at window.jQuery";
        }

        if (typeof window.jQuery.ui == 'undefined') {
            throw "couldn't find jQuery UI at window.jQuery.ui";
        }

        jQuery(function () {
            jQuery(self.legend.list).sortable({
                containment: 'parent',
                tolerance: 'pointer',
                update: function (event, ui) {
                    var series = [];
                    jQuery(self.legend.list).find('li').each(function (index, item) {
                        if (!item.series) return;
                        series.push(item.series);
                    });

                    for (var i = self.graph.series.length - 1; i >= 0; i--) {
                        self.graph.series[i] = series.shift();
                    }

                    self.graph.update();
                }
            });
            jQuery(self.legend.list).disableSelection();
        });

        //hack to make jquery-ui sortable behave
        this.graph.onUpdate(function () {
            var h = window.getComputedStyle(self.legend.element).height;
            self.legend.element.style.height = h;
        });
    };
    Rickshaw.namespace('Rickshaw.Graph.Behavior.Series.Toggle');

    Rickshaw.Graph.Behavior.Series.Toggle = function (args) {

        this.graph = args.graph;
        this.legend = args.legend;

        var self = this;

        this.addAnchor = function (line) {

            var anchor = document.createElement('a');
            anchor.innerHTML = '&#10004;';
            anchor.classList.add('action');
            line.element.insertBefore(anchor, line.element.firstChild);

            anchor.onclick = function (e) {
                if (line.series.disabled) {
                    line.series.enable();
                    line.element.classList.remove('disabled');
                } else {
                    if (this.graph.series.filter(function (s) {
                            return !s.disabled
                        }).length <= 1) return;
                    line.series.disable();
                    line.element.classList.add('disabled');
                }

            }.bind(this);

            var label = line.element.getElementsByTagName('span')[0];
            label.onclick = function (e) {

                var disableAllOtherLines = line.series.disabled;
                if (!disableAllOtherLines) {
                    for (var i = 0; i < self.legend.lines.length; i++) {
                        var l = self.legend.lines[i];
                        if (line.series === l.series) {
                            // noop
                        } else if (l.series.disabled) {
                            // noop
                        } else {
                            disableAllOtherLines = true;
                            break;
                        }
                    }
                }

                // show all or none
                if (disableAllOtherLines) {

                    // these must happen first or else we try ( and probably fail ) to make a no line graph
                    line.series.enable();
                    line.element.classList.remove('disabled');

                    self.legend.lines.forEach(function (l) {
                        if (line.series === l.series) {
                            // noop
                        } else {
                            l.series.disable();
                            l.element.classList.add('disabled');
                        }
                    });

                } else {

                    self.legend.lines.forEach(function (l) {
                        l.series.enable();
                        l.element.classList.remove('disabled');
                    });

                }

            };

        };

        if (this.legend) {

            var $ = jQuery;
            if (typeof $ != 'undefined' && $(this.legend.list).sortable) {

                $(this.legend.list).sortable({
                    start: function (event, ui) {
                        ui.item.bind('no.onclick',
                            function (event) {
                                event.preventDefault();
                            }
                        );
                    },
                    stop: function (event, ui) {
                        setTimeout(function () {
                            ui.item.unbind('no.onclick');
                        }, 250);
                    }
                });
            }

            this.legend.lines.forEach(function (l) {
                self.addAnchor(l);
            });
        }

        this._addBehavior = function () {

            this.graph.series.forEach(function (s) {

                s.disable = function () {

                    if (self.graph.series.length <= 1) {
                        throw('only one series left');
                    }

                    s.disabled = true;
                    self.graph.update();
                };

                s.enable = function () {
                    s.disabled = false;
                    self.graph.update();
                };
            });
        };
        this._addBehavior();

        this.updateBehaviour = function () {
            this._addBehavior()
        };

    };
    Rickshaw.namespace('Rickshaw.Graph.HoverDetail');

    Rickshaw.Graph.HoverDetail = Rickshaw.Class.create({

        initialize: function (args) {

            var graph = this.graph = args.graph;

            this.xFormatter = args.xFormatter || function (x) {
                    return new Date(x * 1000).toUTCString();
                };

            this.yFormatter = args.yFormatter || function (y) {
                    return y === null ? y : y.toFixed(2);
                };

            var element = this.element = document.createElement('div');
            element.className = 'detail';

            this.visible = true;
            graph.element.appendChild(element);

            this.lastEvent = null;
            this._addListeners();

            this.onShow = args.onShow;
            this.onHide = args.onHide;
            this.onRender = args.onRender;

            this.formatter = args.formatter || this.formatter;

        },

        formatter: function (series, x, y, formattedX, formattedY, d) {
            return series.name + ':&nbsp;' + formattedY;
        },

        update: function (e) {

            e = e || this.lastEvent;
            if (!e) return;
            this.lastEvent = e;

            if (!e.target.nodeName.match(/^(path|svg|rect|circle)$/)) return;

            var graph = this.graph;

            var eventX = e.offsetX || e.layerX;
            var eventY = e.offsetY || e.layerY;

            var j = 0;
            var points = [];
            var nearestPoint;

            this.graph.series.active().forEach(function (series) {

                var data = this.graph.stackedData[j++];

                if (!data.length)
                    return;

                var domainX = graph.x.invert(eventX);

                var domainIndexScale = d3.scale.linear()
                    .domain([data[0].x, data.slice(-1)[0].x])
                    .range([0, data.length - 1]);

                var approximateIndex = Math.round(domainIndexScale(domainX));
                if (approximateIndex == data.length - 1) approximateIndex--;

                var dataIndex = Math.min(approximateIndex || 0, data.length - 1);

                for (var i = approximateIndex; i < data.length - 1;) {

                    if (!data[i] || !data[i + 1]) break;

                    if (data[i].x <= domainX && data[i + 1].x > domainX) {
                        dataIndex = Math.abs(domainX - data[i].x) < Math.abs(domainX - data[i + 1].x) ? i : i + 1;
                        break;
                    }

                    if (data[i + 1].x <= domainX) {
                        i++
                    } else {
                        i--
                    }
                }

                if (dataIndex < 0) dataIndex = 0;
                var value = data[dataIndex];

                var distance = Math.sqrt(
                    Math.pow(Math.abs(graph.x(value.x) - eventX), 2) +
                    Math.pow(Math.abs(graph.y(value.y + value.y0) - eventY), 2)
                );

                var xFormatter = series.xFormatter || this.xFormatter;
                var yFormatter = series.yFormatter || this.yFormatter;

                var point = {
                    formattedXValue: xFormatter(value.x),
                    formattedYValue: yFormatter(series.scale ? series.scale.invert(value.y) : value.y),
                    series: series,
                    value: value,
                    distance: distance,
                    order: j,
                    name: series.name
                };

                if (!nearestPoint || distance < nearestPoint.distance) {
                    nearestPoint = point;
                }

                points.push(point);

            }, this);

            if (!nearestPoint)
                return;

            nearestPoint.active = true;

            var domainX = nearestPoint.value.x;
            var formattedXValue = nearestPoint.formattedXValue;

            this.element.innerHTML = '';
            this.element.style.left = graph.x(domainX) + 'px';

            this.visible && this.render({
                points: points,
                detail: points, // for backwards compatibility
                mouseX: eventX,
                mouseY: eventY,
                formattedXValue: formattedXValue,
                domainX: domainX
            });
        },

        hide: function () {
            this.visible = false;
            this.element.classList.add('inactive');

            if (typeof this.onHide == 'function') {
                this.onHide();
            }
        },

        show: function () {
            this.visible = true;
            this.element.classList.remove('inactive');

            if (typeof this.onShow == 'function') {
                this.onShow();
            }
        },

        render: function (args) {

            var graph = this.graph;
            var points = args.points;
            var point = points.filter(function (p) {
                return p.active
            }).shift();

            if (point.value.y === null) return;

            var formattedXValue = point.formattedXValue;
            var formattedYValue = point.formattedYValue;

            this.element.innerHTML = '';
            this.element.style.left = graph.x(point.value.x) + 'px';

            var xLabel = document.createElement('div');

            xLabel.className = 'x_label';
            xLabel.innerHTML = formattedXValue;
            this.element.appendChild(xLabel);

            var item = document.createElement('div');

            item.className = 'item';

            // invert the scale if this series displays using a scale
            var series = point.series;
            var actualY = series.scale ? series.scale.invert(point.value.y) : point.value.y;

            item.innerHTML = this.formatter(series, point.value.x, actualY, formattedXValue, formattedYValue, point);
            item.style.top = this.graph.y(point.value.y0 + point.value.y) + 'px';

            this.element.appendChild(item);

            var dot = document.createElement('div');

            dot.className = 'dot';
            dot.style.top = item.style.top;
            dot.style.borderColor = series.color;

            this.element.appendChild(dot);

            if (point.active) {
                item.classList.add('active');
                dot.classList.add('active');
            }

            // Assume left alignment until the element has been displayed and
            // bounding box calculations are possible.
            var alignables = [xLabel, item];
            alignables.forEach(function (el) {
                el.classList.add('left');
            });

            this.show();

            // If left-alignment results in any error, try right-alignment.
            var leftAlignError = this._calcLayoutError(alignables);
            if (leftAlignError > 0) {
                alignables.forEach(function (el) {
                    el.classList.remove('left');
                    el.classList.add('right');
                });

                // If right-alignment is worse than left alignment, switch back.
                var rightAlignError = this._calcLayoutError(alignables);
                if (rightAlignError > leftAlignError) {
                    alignables.forEach(function (el) {
                        el.classList.remove('right');
                        el.classList.add('left');
                    });
                }
            }

            if (typeof this.onRender == 'function') {
                this.onRender(args);
            }
        },

        _calcLayoutError: function (alignables) {
            // Layout error is calculated as the number of linear pixels by which
            // an alignable extends past the left or right edge of the parent.
            var parentRect = this.element.parentNode.getBoundingClientRect();

            var error = 0;
            var alignRight = alignables.forEach(function (el) {
                var rect = el.getBoundingClientRect();
                if (!rect.width) {
                    return;
                }

                if (rect.right > parentRect.right) {
                    error += rect.right - parentRect.right;
                }

                if (rect.left < parentRect.left) {
                    error += parentRect.left - rect.left;
                }
            });
            return error;
        },

        _addListeners: function () {

            this.graph.element.addEventListener(
                'mousemove',
                function (e) {
                    this.visible = true;
                    this.update(e);
                }.bind(this),
                false
            );

            this.graph.onUpdate(function () {
                this.update()
            }.bind(this));

            this.graph.element.addEventListener(
                'mouseout',
                function (e) {
                    if (e.relatedTarget && !(e.relatedTarget.compareDocumentPosition(this.graph.element) & Node.DOCUMENT_POSITION_CONTAINS)) {
                        this.hide();
                    }
                }.bind(this),
                false
            );
        }
    });
    Rickshaw.namespace('Rickshaw.Graph.JSONP');

    Rickshaw.Graph.JSONP = Rickshaw.Class.create(Rickshaw.Graph.Ajax, {

        request: function () {

            jQuery.ajax({
                url: this.dataURL,
                dataType: 'jsonp',
                success: this.success.bind(this),
                error: this.error.bind(this)
            });
        }
    });
    Rickshaw.namespace('Rickshaw.Graph.Legend');

    Rickshaw.Graph.Legend = Rickshaw.Class.create({

        className: 'rickshaw_legend',

        initialize: function (args) {
            this.element = args.element;
            this.graph = args.graph;
            this.naturalOrder = args.naturalOrder;

            this.element.classList.add(this.className);

            this.list = document.createElement('ul');
            this.element.appendChild(this.list);

            this.render();

            // we could bind this.render.bind(this) here
            // but triggering the re-render would lose the added
            // behavior of the series toggle
            this.graph.onUpdate(function () {
            });
        },

        render: function () {
            var self = this;

            while (this.list.firstChild) {
                this.list.removeChild(this.list.firstChild);
            }
            this.lines = [];

            var series = this.graph.series
                .map(function (s) {
                    return s
                });

            if (!this.naturalOrder) {
                series = series.reverse();
            }

            series.forEach(function (s) {
                self.addLine(s);
            });


        },

        addLine: function (series) {
            var line = document.createElement('li');
            line.className = 'line';
            if (series.disabled) {
                line.className += ' disabled';
            }
            if (series.className) {
                d3.select(line).classed(series.className, true);
            }
            var swatch = document.createElement('div');
            swatch.className = 'swatch';
            swatch.style.backgroundColor = series.color;

            line.appendChild(swatch);

            var label = document.createElement('span');
            label.className = 'label';
            label.innerHTML = series.name;

            line.appendChild(label);
            this.list.appendChild(line);

            line.series = series;

            if (series.noLegend) {
                line.style.display = 'none';
            }

            var _line = {element: line, series: series};
            if (this.shelving) {
                this.shelving.addAnchor(_line);
                this.shelving.updateBehaviour();
            }
            if (this.highlighter) {
                this.highlighter.addHighlightEvents(_line);
            }
            this.lines.push(_line);
            return line;
        }
    });

    Rickshaw.namespace('Rickshaw.Graph.RangeSlider');

    Rickshaw.Graph.RangeSlider = Rickshaw.Class.create({

        initialize: function (args) {

            var element = this.element = args.element;
            var graph = this.graph = args.graph;

            this.slideCallbacks = [];

            this.build();

            graph.onUpdate(function () {
                this.update()
            }.bind(this));
        },

        build: function () {

            var element = this.element;
            var graph = this.graph;
            var $ = jQuery;

            var domain = graph.dataDomain();
            var self = this;

            $(function () {
                $(element).slider({
                    range: true,
                    min: domain[0],
                    max: domain[1],
                    values: [
                        domain[0],
                        domain[1]
                    ],
                    slide: function (event, ui) {

                        if (ui.values[1] <= ui.values[0]) return;

                        graph.window.xMin = ui.values[0];
                        graph.window.xMax = ui.values[1];
                        graph.update();

                        var domain = graph.dataDomain();

                        // if we're at an extreme, stick there
                        if (domain[0] == ui.values[0]) {
                            graph.window.xMin = undefined;
                        }

                        if (domain[1] == ui.values[1]) {
                            graph.window.xMax = undefined;
                        }

                        self.slideCallbacks.forEach(function (callback) {
                            callback(graph, graph.window.xMin, graph.window.xMax);
                        });
                    }
                });
            });

            $(element)[0].style.width = graph.width + 'px';

        },

        update: function () {

            var element = this.element;
            var graph = this.graph;
            var $ = jQuery;

            var values = $(element).slider('option', 'values');

            var domain = graph.dataDomain();

            $(element).slider('option', 'min', domain[0]);
            $(element).slider('option', 'max', domain[1]);

            if (graph.window.xMin == null) {
                values[0] = domain[0];
            }
            if (graph.window.xMax == null) {
                values[1] = domain[1];
            }

            $(element).slider('option', 'values', values);
        },

        onSlide: function (callback) {
            this.slideCallbacks.push(callback);
        }
    });

    Rickshaw.namespace('Rickshaw.Graph.RangeSlider.Preview');

    Rickshaw.Graph.RangeSlider.Preview = Rickshaw.Class.create({

        initialize: function (args) {

            if (!args.element) throw "Rickshaw.Graph.RangeSlider.Preview needs a reference to an element";
            if (!args.graph && !args.graphs) throw "Rickshaw.Graph.RangeSlider.Preview needs a reference to an graph or an array of graphs";

            this.element = args.element;
            this.element.style.position = 'relative';

            this.graphs = args.graph ? [args.graph] : args.graphs;

            this.defaults = {
                height: 75,
                width: 400,
                gripperColor: undefined,
                frameTopThickness: 3,
                frameHandleThickness: 10,
                frameColor: "#d4d4d4",
                frameOpacity: 1,
                minimumFrameWidth: 0,
                heightRatio: 0.2
            };

            this.heightRatio = args.heightRatio || this.defaults.heightRatio;
            this.defaults.gripperColor = d3.rgb(this.defaults.frameColor).darker().toString();

            this.configureCallbacks = [];
            this.slideCallbacks = [];

            this.previews = [];

            if (!args.width) this.widthFromGraph = true;
            if (!args.height) this.heightFromGraph = true;

            if (this.widthFromGraph || this.heightFromGraph) {
                this.graphs[0].onConfigure(function () {
                    this.configure(args);
                    this.render();
                }.bind(this));
            }

            args.width = args.width || this.graphs[0].width || this.defaults.width;
            args.height = args.height || this.graphs[0].height * this.heightRatio || this.defaults.height;

            this.configure(args);
            this.render();
        },

        onSlide: function (callback) {
            this.slideCallbacks.push(callback);
        },

        onConfigure: function (callback) {
            this.configureCallbacks.push(callback);
        },

        configure: function (args) {

            this.config = this.config || {};

            this.configureCallbacks.forEach(function (callback) {
                callback(args);
            });

            Rickshaw.keys(this.defaults).forEach(function (k) {
                this.config[k] = k in args ? args[k]
                    : k in this.config ? this.config[k]
                    : this.defaults[k];
            }, this);

            if ('width' in args || 'height' in args) {

                if (this.widthFromGraph) {
                    this.config.width = this.graphs[0].width;
                }

                if (this.heightFromGraph) {
                    this.config.height = this.graphs[0].height * this.heightRatio;
                    this.previewHeight = this.config.height;
                }

                this.previews.forEach(function (preview) {

                    var height = this.previewHeight / this.graphs.length - this.config.frameTopThickness * 2;
                    var width = this.config.width - this.config.frameHandleThickness * 2;
                    preview.setSize({width: width, height: height});

                    if (this.svg) {
                        var svgHeight = height + this.config.frameHandleThickness * 2;
                        var svgWidth = width + this.config.frameHandleThickness * 2;
                        this.svg.style("width", svgWidth + "px");
                        this.svg.style("height", svgHeight + "px");
                    }
                }, this);
            }
        },

        render: function () {

            var self = this;

            this.svg = d3.select(this.element)
                .selectAll("svg.rickshaw_range_slider_preview")
                .data([null]);

            this.previewHeight = this.config.height - (this.config.frameTopThickness * 2);
            this.previewWidth = this.config.width - (this.config.frameHandleThickness * 2);

            this.currentFrame = [0, this.previewWidth];

            var buildGraph = function (parent, index) {

                var graphArgs = Rickshaw.extend({}, parent.config);
                var height = self.previewHeight / self.graphs.length;
                var renderer = parent.renderer.name;

                Rickshaw.extend(graphArgs, {
                    element: this.appendChild(document.createElement("div")),
                    height: height,
                    width: self.previewWidth,
                    series: parent.series,
                    renderer: renderer
                });

                var graph = new Rickshaw.Graph(graphArgs);
                self.previews.push(graph);

                parent.onUpdate(function () {
                    graph.render();
                    self.render()
                });

                parent.onConfigure(function (args) {
                    // don't propagate height
                    delete args.height;
                    args.width = args.width - self.config.frameHandleThickness * 2;
                    graph.configure(args);
                    graph.render();
                });

                graph.render();
            };

            var graphContainer = d3.select(this.element)
                .selectAll("div.rickshaw_range_slider_preview_container")
                .data(this.graphs);

            var translateCommand = "translate(" +
                this.config.frameHandleThickness + "px, " +
                this.config.frameTopThickness + "px)";

            graphContainer.enter()
                .append("div")
                .classed("rickshaw_range_slider_preview_container", true)
                .style("-webkit-transform", translateCommand)
                .style("-moz-transform", translateCommand)
                .style("-ms-transform", translateCommand)
                .style("transform", translateCommand)
                .each(buildGraph);

            graphContainer.exit()
                .remove();

            // Use the first graph as the "master" for the frame state
            var masterGraph = this.graphs[0];

            var domainScale = d3.scale.linear()
                .domain([0, this.previewWidth])
                .range(masterGraph.dataDomain());

            var currentWindow = [masterGraph.window.xMin, masterGraph.window.xMax];

            this.currentFrame[0] = currentWindow[0] === undefined ?
                0 : Math.round(domainScale.invert(currentWindow[0]));

            if (this.currentFrame[0] < 0) this.currentFrame[0] = 0;

            this.currentFrame[1] = currentWindow[1] === undefined ?
                this.previewWidth : domainScale.invert(currentWindow[1]);

            if (this.currentFrame[1] - this.currentFrame[0] < self.config.minimumFrameWidth) {
                this.currentFrame[1] = (this.currentFrame[0] || 0) + self.config.minimumFrameWidth;
            }

            this.svg.enter()
                .append("svg")
                .classed("rickshaw_range_slider_preview", true)
                .style("height", this.config.height + "px")
                .style("width", this.config.width + "px")
                .style("position", "absolute")
                .style("top", 0);

            this._renderDimming();
            this._renderFrame();
            this._renderGrippers();
            this._renderHandles();
            this._renderMiddle();

            this._registerMouseEvents();
        },

        _renderDimming: function () {

            var element = this.svg
                .selectAll("path.dimming")
                .data([null]);

            element.enter()
                .append("path")
                .attr("fill", "white")
                .attr("fill-opacity", "0.7")
                .attr("fill-rule", "evenodd")
                .classed("dimming", true);

            var path = "";
            path += " M " + this.config.frameHandleThickness + " " + this.config.frameTopThickness;
            path += " h " + this.previewWidth;
            path += " v " + this.previewHeight;
            path += " h " + -this.previewWidth;
            path += " z ";
            path += " M " + Math.max(this.currentFrame[0], this.config.frameHandleThickness) + " " + this.config.frameTopThickness;
            path += " H " + Math.min(this.currentFrame[1] + this.config.frameHandleThickness * 2, this.previewWidth + this.config.frameHandleThickness);
            path += " v " + this.previewHeight;
            path += " H " + Math.max(this.currentFrame[0], this.config.frameHandleThickness);
            path += " z";

            element.attr("d", path);
        },

        _renderFrame: function () {

            var element = this.svg
                .selectAll("path.frame")
                .data([null]);

            element.enter()
                .append("path")
                .attr("stroke", "white")
                .attr("stroke-width", "1px")
                .attr("stroke-linejoin", "round")
                .attr("fill", this.config.frameColor)
                .attr("fill-opacity", this.config.frameOpacity)
                .attr("fill-rule", "evenodd")
                .classed("frame", true);

            var path = "";
            path += " M " + this.currentFrame[0] + " 0";
            path += " H " + (this.currentFrame[1] + (this.config.frameHandleThickness * 2));
            path += " V " + this.config.height;
            path += " H " + (this.currentFrame[0]);
            path += " z";
            path += " M " + (this.currentFrame[0] + this.config.frameHandleThickness) + " " + this.config.frameTopThickness;
            path += " H " + (this.currentFrame[1] + this.config.frameHandleThickness);
            path += " v " + this.previewHeight;
            path += " H " + (this.currentFrame[0] + this.config.frameHandleThickness);
            path += " z";

            element.attr("d", path);
        },

        _renderGrippers: function () {

            var gripper = this.svg.selectAll("path.gripper")
                .data([null]);

            gripper.enter()
                .append("path")
                .attr("stroke", this.config.gripperColor)
                .classed("gripper", true);

            var path = "";

            [0.4, 0.6].forEach(function (spacing) {
                path += " M " + Math.round((this.currentFrame[0] + (this.config.frameHandleThickness * spacing))) + " " + Math.round(this.config.height * 0.3);
                path += " V " + Math.round(this.config.height * 0.7);
                path += " M " + Math.round((this.currentFrame[1] + (this.config.frameHandleThickness * (1 + spacing)))) + " " + Math.round(this.config.height * 0.3);
                path += " V " + Math.round(this.config.height * 0.7);
            }.bind(this));

            gripper.attr("d", path);
        },

        _renderHandles: function () {

            var leftHandle = this.svg.selectAll("rect.left_handle")
                .data([null]);

            leftHandle.enter()
                .append("rect")
                .attr('width', this.config.frameHandleThickness)
                .style("cursor", "ew-resize")
                .style("fill-opacity", "0")
                .classed("left_handle", true);

            leftHandle
                .attr('x', this.currentFrame[0])
                .attr('height', this.config.height);

            var rightHandle = this.svg.selectAll("rect.right_handle")
                .data([null]);

            rightHandle.enter()
                .append("rect")
                .attr('width', this.config.frameHandleThickness)
                .style("cursor", "ew-resize")
                .style("fill-opacity", "0")
                .classed("right_handle", true);

            rightHandle
                .attr('x', this.currentFrame[1] + this.config.frameHandleThickness)
                .attr('height', this.config.height);
        },

        _renderMiddle: function () {

            var middleHandle = this.svg.selectAll("rect.middle_handle")
                .data([null]);

            middleHandle.enter()
                .append("rect")
                .style("cursor", "move")
                .style("fill-opacity", "0")
                .classed("middle_handle", true);

            middleHandle
                .attr('width', Math.max(0, this.currentFrame[1] - this.currentFrame[0]))
                .attr('x', this.currentFrame[0] + this.config.frameHandleThickness)
                .attr('height', this.config.height);
        },

        _registerMouseEvents: function () {

            var element = d3.select(this.element);

            var drag = {
                target: null,
                start: null,
                stop: null,
                left: false,
                right: false,
                rigid: false
            };

            var self = this;

            function onMousemove(datum, index) {

                drag.stop = self._getClientXFromEvent(d3.event, drag);
                var distanceTraveled = drag.stop - drag.start;
                var frameAfterDrag = self.frameBeforeDrag.slice(0);
                var minimumFrameWidth = self.config.minimumFrameWidth;

                if (drag.rigid) {
                    minimumFrameWidth = self.frameBeforeDrag[1] - self.frameBeforeDrag[0];
                }
                if (drag.left) {
                    frameAfterDrag[0] = Math.max(frameAfterDrag[0] + distanceTraveled, 0);
                }
                if (drag.right) {
                    frameAfterDrag[1] = Math.min(frameAfterDrag[1] + distanceTraveled, self.previewWidth);
                }

                var currentFrameWidth = frameAfterDrag[1] - frameAfterDrag[0];

                if (currentFrameWidth <= minimumFrameWidth) {

                    if (drag.left) {
                        frameAfterDrag[0] = frameAfterDrag[1] - minimumFrameWidth;
                    }
                    if (drag.right) {
                        frameAfterDrag[1] = frameAfterDrag[0] + minimumFrameWidth;
                    }
                    if (frameAfterDrag[0] <= 0) {
                        frameAfterDrag[1] -= frameAfterDrag[0];
                        frameAfterDrag[0] = 0;
                    }
                    if (frameAfterDrag[1] >= self.previewWidth) {
                        frameAfterDrag[0] -= (frameAfterDrag[1] - self.previewWidth);
                        frameAfterDrag[1] = self.previewWidth;
                    }
                }

                self.graphs.forEach(function (graph) {

                    var domainScale = d3.scale.linear()
                        .interpolate(d3.interpolateNumber)
                        .domain([0, self.previewWidth])
                        .range(graph.dataDomain());

                    var windowAfterDrag = [
                        domainScale(frameAfterDrag[0]),
                        domainScale(frameAfterDrag[1])
                    ];

                    self.slideCallbacks.forEach(function (callback) {
                        callback(graph, windowAfterDrag[0], windowAfterDrag[1]);
                    });

                    if (frameAfterDrag[0] === 0) {
                        windowAfterDrag[0] = undefined;
                    }
                    if (frameAfterDrag[1] === self.previewWidth) {
                        windowAfterDrag[1] = undefined;
                    }
                    graph.window.xMin = windowAfterDrag[0];
                    graph.window.xMax = windowAfterDrag[1];

                    graph.update();
                });
            }

            function onMousedown() {
                drag.target = d3.event.target;
                drag.start = self._getClientXFromEvent(d3.event, drag);
                self.frameBeforeDrag = self.currentFrame.slice();
                d3.event.preventDefault ? d3.event.preventDefault() : d3.event.returnValue = false;
                d3.select(document).on("mousemove.rickshaw_range_slider_preview", onMousemove);
                d3.select(document).on("mouseup.rickshaw_range_slider_preview", onMouseup);
                d3.select(document).on("touchmove.rickshaw_range_slider_preview", onMousemove);
                d3.select(document).on("touchend.rickshaw_range_slider_preview", onMouseup);
                d3.select(document).on("touchcancel.rickshaw_range_slider_preview", onMouseup);
            }

            function onMousedownLeftHandle(datum, index) {
                drag.left = true;
                onMousedown();
            }

            function onMousedownRightHandle(datum, index) {
                drag.right = true;
                onMousedown();
            }

            function onMousedownMiddleHandle(datum, index) {
                drag.left = true;
                drag.right = true;
                drag.rigid = true;
                onMousedown();
            }

            function onMouseup(datum, index) {
                d3.select(document).on("mousemove.rickshaw_range_slider_preview", null);
                d3.select(document).on("mouseup.rickshaw_range_slider_preview", null);
                d3.select(document).on("touchmove.rickshaw_range_slider_preview", null);
                d3.select(document).on("touchend.rickshaw_range_slider_preview", null);
                d3.select(document).on("touchcancel.rickshaw_range_slider_preview", null);
                delete self.frameBeforeDrag;
                drag.left = false;
                drag.right = false;
                drag.rigid = false;
            }

            element.select("rect.left_handle").on("mousedown", onMousedownLeftHandle);
            element.select("rect.right_handle").on("mousedown", onMousedownRightHandle);
            element.select("rect.middle_handle").on("mousedown", onMousedownMiddleHandle);
            element.select("rect.left_handle").on("touchstart", onMousedownLeftHandle);
            element.select("rect.right_handle").on("touchstart", onMousedownRightHandle);
            element.select("rect.middle_handle").on("touchstart", onMousedownMiddleHandle);
        },

        _getClientXFromEvent: function (event, drag) {

            switch (event.type) {
                case 'touchstart':
                case 'touchmove':
                    var touchList = event.changedTouches;
                    var touch = null;
                    for (var touchIndex = 0; touchIndex < touchList.length; touchIndex++) {
                        if (touchList[touchIndex].target === drag.target) {
                            touch = touchList[touchIndex];
                            break;
                        }
                    }
                    return touch !== null ? touch.clientX : undefined;

                default:
                    return event.clientX;
            }
        }
    });

    Rickshaw.namespace("Rickshaw.Graph.Renderer");

    Rickshaw.Graph.Renderer = Rickshaw.Class.create({

        initialize: function (args) {
            this.graph = args.graph;
            this.tension = args.tension || this.tension;
            this.configure(args);
        },

        seriesPathFactory: function () {
            //implement in subclass
        },

        seriesStrokeFactory: function () {
            // implement in subclass
        },

        defaults: function () {
            return {
                tension: 0.8,
                strokeWidth: 2,
                unstack: true,
                padding: {top: 0.01, right: 0, bottom: 0.01, left: 0},
                stroke: false,
                fill: false
            };
        },

        domain: function (data) {
            // Requires that at least one series contains some data
            var stackedData = data || this.graph.stackedData || this.graph.stackData();

            var xMin = +Infinity;
            var xMax = -Infinity;

            var yMin = +Infinity;
            var yMax = -Infinity;

            stackedData.forEach(function (series) {

                series.forEach(function (d) {

                    if (d.y == null) return;

                    var y = d.y + d.y0;

                    if (y < yMin) yMin = y;
                    if (y > yMax) yMax = y;
                });

                if (!series.length) return;

                if (series[0].x < xMin) xMin = series[0].x;
                if (series[series.length - 1].x > xMax) xMax = series[series.length - 1].x;
            });

            xMin -= (xMax - xMin) * this.padding.left;
            xMax += (xMax - xMin) * this.padding.right;

            yMin = this.graph.min === 'auto' ? yMin : this.graph.min || 0;
            yMax = this.graph.max === undefined ? yMax : this.graph.max;

            if (this.graph.min === 'auto' || yMin < 0) {
                yMin -= (yMax - yMin) * this.padding.bottom;
            }

            if (this.graph.max === undefined) {
                yMax += (yMax - yMin) * this.padding.top;
            }

            return {x: [xMin, xMax], y: [yMin, yMax]};
        },

        render: function (args) {

            args = args || {};

            var graph = this.graph;
            var series = args.series || graph.series;

            var vis = args.vis || graph.vis;
            vis.selectAll('*').remove();

            var data = series
                .filter(function (s) {
                    return !s.disabled
                })
                .map(function (s) {
                    return s.stack
                });

            var pathNodes = vis.selectAll("path.path")
                .data(data)
                .enter().append("svg:path")
                .classed('path', true)
                .attr("d", this.seriesPathFactory());

            if (this.stroke) {
                var strokeNodes = vis.selectAll('path.stroke')
                    .data(data)
                    .enter().append("svg:path")
                    .classed('stroke', true)
                    .attr("d", this.seriesStrokeFactory());
            }

            var i = 0;
            series.forEach(function (series) {
                if (series.disabled) return;
                series.path = pathNodes[0][i];
                if (this.stroke) series.stroke = strokeNodes[0][i];
                this._styleSeries(series);
                i++;
            }, this);

        },

        _styleSeries: function (series) {

            var fill = this.fill ? series.color : 'none';
            var stroke = this.stroke ? series.color : 'none';

            series.path.setAttribute('fill', fill);
            series.path.setAttribute('stroke', stroke);
            series.path.setAttribute('stroke-width', this.strokeWidth);

            if (series.className) {
                d3.select(series.path).classed(series.className, true);
            }
            if (series.className && this.stroke) {
                d3.select(series.stroke).classed(series.className, true);
            }
        },

        configure: function (args) {

            args = args || {};

            Rickshaw.keys(this.defaults()).forEach(function (key) {

                if (!args.hasOwnProperty(key)) {
                    this[key] = this[key] || this.graph[key] || this.defaults()[key];
                    return;
                }

                if (typeof this.defaults()[key] == 'object') {

                    Rickshaw.keys(this.defaults()[key]).forEach(function (k) {

                        this[key][k] =
                            args[key][k] !== undefined ? args[key][k] :
                                this[key][k] !== undefined ? this[key][k] :
                                    this.defaults()[key][k];
                    }, this);

                } else {
                    this[key] =
                        args[key] !== undefined ? args[key] :
                            this[key] !== undefined ? this[key] :
                                this.graph[key] !== undefined ? this.graph[key] :
                                    this.defaults()[key];
                }

            }, this);
        },

        setStrokeWidth: function (strokeWidth) {
            if (strokeWidth !== undefined) {
                this.strokeWidth = strokeWidth;
            }
        },

        setTension: function (tension) {
            if (tension !== undefined) {
                this.tension = tension;
            }
        }
    });

    Rickshaw.namespace('Rickshaw.Graph.Renderer.Line');

    Rickshaw.Graph.Renderer.Line = Rickshaw.Class.create(Rickshaw.Graph.Renderer, {

        name: 'line',

        defaults: function ($super) {

            return Rickshaw.extend($super(), {
                unstack: true,
                fill: false,
                stroke: true
            });
        },

        seriesPathFactory: function () {

            var graph = this.graph;

            var factory = d3.svg.line()
                .x(function (d) {
                    return graph.x(d.x)
                })
                .y(function (d) {
                    return graph.y(d.y)
                })
                .interpolate(this.graph.interpolation).tension(this.tension);

            factory.defined && factory.defined(function (d) {
                return d.y !== null
            });
            return factory;
        }
    });

    Rickshaw.namespace('Rickshaw.Graph.Renderer.Stack');

    Rickshaw.Graph.Renderer.Stack = Rickshaw.Class.create(Rickshaw.Graph.Renderer, {

        name: 'stack',

        defaults: function ($super) {

            return Rickshaw.extend($super(), {
                fill: true,
                stroke: false,
                unstack: false
            });
        },

        seriesPathFactory: function () {

            var graph = this.graph;

            var factory = d3.svg.area()
                .x(function (d) {
                    return graph.x(d.x)
                })
                .y0(function (d) {
                    return graph.y(d.y0)
                })
                .y1(function (d) {
                    return graph.y(d.y + d.y0)
                })
                .interpolate(this.graph.interpolation).tension(this.tension);

            factory.defined && factory.defined(function (d) {
                return d.y !== null
            });
            return factory;
        }
    });

    Rickshaw.namespace('Rickshaw.Graph.Renderer.Bar');

    Rickshaw.Graph.Renderer.Bar = Rickshaw.Class.create(Rickshaw.Graph.Renderer, {

        name: 'bar',

        defaults: function ($super) {

            var defaults = Rickshaw.extend($super(), {
                gapSize: 0.05,
                unstack: false
            });

            delete defaults.tension;
            return defaults;
        },

        initialize: function ($super, args) {
            args = args || {};
            this.gapSize = args.gapSize || this.gapSize;
            $super(args);
        },

        domain: function ($super) {

            var domain = $super();

            var frequentInterval = this._frequentInterval(this.graph.stackedData.slice(-1).shift());
            domain.x[1] += Number(frequentInterval.magnitude);

            return domain;
        },

        barWidth: function (series) {

            var frequentInterval = this._frequentInterval(series.stack);
            var barWidth = this.graph.x.magnitude(frequentInterval.magnitude) * (1 - this.gapSize);

            return barWidth;
        },

        render: function (args) {

            args = args || {};

            var graph = this.graph;
            var series = args.series || graph.series;

            var vis = args.vis || graph.vis;
            vis.selectAll('*').remove();

            var barWidth = this.barWidth(series.active()[0]);
            var barXOffset = 0;

            var activeSeriesCount = series.filter(function (s) {
                return !s.disabled;
            }).length;
            var seriesBarWidth = this.unstack ? barWidth / activeSeriesCount : barWidth;

            var transform = function (d) {
                // add a matrix transform for negative values
                var matrix = [1, 0, 0, (d.y < 0 ? -1 : 1), 0, (d.y < 0 ? graph.y.magnitude(Math.abs(d.y)) * 2 : 0)];
                return "matrix(" + matrix.join(',') + ")";
            };

            series.forEach(function (series) {

                if (series.disabled) return;

                var barWidth = this.barWidth(series);

                var nodes = vis.selectAll("path")
                    .data(series.stack.filter(function (d) {
                        return d.y !== null
                    }))
                    .enter().append("svg:rect")
                    .attr("x", function (d) {
                        return graph.x(d.x) + barXOffset
                    })
                    .attr("y", function (d) {
                        return (graph.y(d.y0 + Math.abs(d.y))) * (d.y < 0 ? -1 : 1 )
                    })
                    .attr("width", seriesBarWidth)
                    .attr("height", function (d) {
                        return graph.y.magnitude(Math.abs(d.y))
                    })
                    .attr("transform", transform);

                Array.prototype.forEach.call(nodes[0], function (n) {
                    n.setAttribute('fill', series.color);
                });

                if (this.unstack) barXOffset += seriesBarWidth;

            }, this);
        },

        _frequentInterval: function (data) {

            var intervalCounts = {};

            for (var i = 0; i < data.length - 1; i++) {
                var interval = data[i + 1].x - data[i].x;
                intervalCounts[interval] = intervalCounts[interval] || 0;
                intervalCounts[interval]++;
            }

            var frequentInterval = {count: 0, magnitude: 1};

            Rickshaw.keys(intervalCounts).forEach(function (i) {
                if (frequentInterval.count < intervalCounts[i]) {
                    frequentInterval = {
                        count: intervalCounts[i],
                        magnitude: i
                    };
                }
            });

            return frequentInterval;
        }
    });

    Rickshaw.namespace('Rickshaw.Graph.Renderer.Area');

    Rickshaw.Graph.Renderer.Area = Rickshaw.Class.create(Rickshaw.Graph.Renderer, {

        name: 'area',

        defaults: function ($super) {

            return Rickshaw.extend($super(), {
                unstack: false,
                fill: false,
                stroke: false
            });
        },

        seriesPathFactory: function () {

            var graph = this.graph;

            var factory = d3.svg.area()
                .x(function (d) {
                    return graph.x(d.x)
                })
                .y0(function (d) {
                    return graph.y(d.y0)
                })
                .y1(function (d) {
                    return graph.y(d.y + d.y0)
                })
                .interpolate(graph.interpolation).tension(this.tension);

            factory.defined && factory.defined(function (d) {
                return d.y !== null
            });
            return factory;
        },

        seriesStrokeFactory: function () {

            var graph = this.graph;

            var factory = d3.svg.line()
                .x(function (d) {
                    return graph.x(d.x)
                })
                .y(function (d) {
                    return graph.y(d.y + d.y0)
                })
                .interpolate(graph.interpolation).tension(this.tension);

            factory.defined && factory.defined(function (d) {
                return d.y !== null
            });
            return factory;
        },

        render: function (args) {

            args = args || {};

            var graph = this.graph;
            var series = args.series || graph.series;

            var vis = args.vis || graph.vis;
            vis.selectAll('*').remove();

            // insert or stacked areas so strokes lay on top of areas
            var method = this.unstack ? 'append' : 'insert';

            var data = series
                .filter(function (s) {
                    return !s.disabled
                })
                .map(function (s) {
                    return s.stack
                });

            var nodes = vis.selectAll("path")
                .data(data)
                .enter()[method]("svg:g", 'g');

            nodes.append("svg:path")
                .attr("d", this.seriesPathFactory())
                .attr("class", 'area');

            if (this.stroke) {
                nodes.append("svg:path")
                    .attr("d", this.seriesStrokeFactory())
                    .attr("class", 'line');
            }

            var i = 0;
            series.forEach(function (series) {
                if (series.disabled) return;
                series.path = nodes[0][i++];
                this._styleSeries(series);
            }, this);
        },

        _styleSeries: function (series) {

            if (!series.path) return;

            d3.select(series.path).select('.area')
                .attr('fill', series.color);

            if (this.stroke) {
                d3.select(series.path).select('.line')
                    .attr('fill', 'none')
                    .attr('stroke', series.stroke || d3.interpolateRgb(series.color, 'black')(0.125))
                    .attr('stroke-width', this.strokeWidth);
            }

            if (series.className) {
                series.path.setAttribute('class', series.className);
            }
        }
    });

    Rickshaw.namespace('Rickshaw.Graph.Renderer.ScatterPlot');

    Rickshaw.Graph.Renderer.ScatterPlot = Rickshaw.Class.create(Rickshaw.Graph.Renderer, {

        name: 'scatterplot',

        defaults: function ($super) {

            return Rickshaw.extend($super(), {
                unstack: true,
                fill: true,
                stroke: false,
                padding: {top: 0.01, right: 0.01, bottom: 0.01, left: 0.01},
                dotSize: 4
            });
        },

        initialize: function ($super, args) {
            $super(args);
        },

        render: function (args) {

            args = args || {};

            var graph = this.graph;

            var series = args.series || graph.series;
            var vis = args.vis || graph.vis;

            var dotSize = this.dotSize;

            vis.selectAll('*').remove();

            series.forEach(function (series) {

                if (series.disabled) return;

                var nodes = vis.selectAll("path")
                    .data(series.stack.filter(function (d) {
                        return d.y !== null
                    }))
                    .enter().append("svg:circle")
                    .attr("cx", function (d) {
                        return graph.x(d.x)
                    })
                    .attr("cy", function (d) {
                        return graph.y(d.y)
                    })
                    .attr("r", function (d) {
                        return ("r" in d) ? d.r : dotSize
                    });
                if (series.className) {
                    nodes.classed(series.className, true);
                }

                Array.prototype.forEach.call(nodes[0], function (n) {
                    n.setAttribute('fill', series.color);
                });

            }, this);
        }
    });
    Rickshaw.namespace('Rickshaw.Graph.Renderer.Multi');

    Rickshaw.Graph.Renderer.Multi = Rickshaw.Class.create(Rickshaw.Graph.Renderer, {

        name: 'multi',

        initialize: function ($super, args) {

            $super(args);
        },

        defaults: function ($super) {

            return Rickshaw.extend($super(), {
                unstack: true,
                fill: false,
                stroke: true
            });
        },

        configure: function ($super, args) {

            args = args || {};
            this.config = args;
            $super(args);
        },

        domain: function ($super) {

            this.graph.stackData();

            var domains = [];

            var groups = this._groups();
            this._stack(groups);

            groups.forEach(function (group) {

                var data = group.series
                    .filter(function (s) {
                        return !s.disabled
                    })
                    .map(function (s) {
                        return s.stack
                    });

                if (!data.length) return;

                var domain = null;
                if (group.renderer && group.renderer.domain) {
                    domain = group.renderer.domain(data);
                }
                else {
                    domain = $super(data);
                }
                domains.push(domain);
            });

            var xMin = d3.min(domains.map(function (d) {
                return d.x[0]
            }));
            var xMax = d3.max(domains.map(function (d) {
                return d.x[1]
            }));
            var yMin = d3.min(domains.map(function (d) {
                return d.y[0]
            }));
            var yMax = d3.max(domains.map(function (d) {
                return d.y[1]
            }));

            return {x: [xMin, xMax], y: [yMin, yMax]};
        },

        _groups: function () {

            var graph = this.graph;

            var renderGroups = {};

            graph.series.forEach(function (series) {

                if (series.disabled) return;

                if (!renderGroups[series.renderer]) {

                    var ns = "http://www.w3.org/2000/svg";
                    var vis = document.createElementNS(ns, 'g');

                    graph.vis[0][0].appendChild(vis);

                    var renderer = graph._renderers[series.renderer];

                    var config = {};

                    var defaults = [this.defaults(), renderer.defaults(), this.config, this.graph];
                    defaults.forEach(function (d) {
                        Rickshaw.extend(config, d)
                    });

                    renderer.configure(config);

                    renderGroups[series.renderer] = {
                        renderer: renderer,
                        series: [],
                        vis: d3.select(vis)
                    };
                }

                renderGroups[series.renderer].series.push(series);

            }, this);

            var groups = [];

            Object.keys(renderGroups).forEach(function (key) {
                var group = renderGroups[key];
                groups.push(group);
            });

            return groups;
        },

        _stack: function (groups) {

            groups.forEach(function (group) {

                var series = group.series
                    .filter(function (series) {
                        return !series.disabled
                    });

                var data = series
                    .map(function (series) {
                        return series.stack
                    });

                if (!group.renderer.unstack) {

                    var layout = d3.layout.stack();
                    var stackedData = Rickshaw.clone(layout(data));

                    series.forEach(function (series, index) {
                        series._stack = Rickshaw.clone(stackedData[index]);
                    });
                }

            }, this);

            return groups;

        },

        render: function () {

            this.graph.series.forEach(function (series) {
                if (!series.renderer) {
                    throw new Error("Each series needs a renderer for graph 'multi' renderer");
                }
            });

            this.graph.vis.selectAll('*').remove();

            var groups = this._groups();
            groups = this._stack(groups);

            groups.forEach(function (group) {

                var series = group.series
                    .filter(function (series) {
                        return !series.disabled
                    });

                series.active = function () {
                    return series
                };

                group.renderer.render({series: series, vis: group.vis});
                series.forEach(function (s) {
                    s.stack = s._stack || s.stack || s.data;
                });
            });
        }

    });
    Rickshaw.namespace('Rickshaw.Graph.Renderer.LinePlot');

    Rickshaw.Graph.Renderer.LinePlot = Rickshaw.Class.create(Rickshaw.Graph.Renderer, {

        name: 'lineplot',

        defaults: function ($super) {

            return Rickshaw.extend($super(), {
                unstack: true,
                fill: false,
                stroke: true,
                padding: {top: 0.01, right: 0.01, bottom: 0.01, left: 0.01},
                dotSize: 3,
                strokeWidth: 2
            });
        },

        seriesPathFactory: function () {

            var graph = this.graph;

            var factory = d3.svg.line()
                .x(function (d) {
                    return graph.x(d.x)
                })
                .y(function (d) {
                    return graph.y(d.y)
                })
                .interpolate(this.graph.interpolation).tension(this.tension);

            factory.defined && factory.defined(function (d) {
                return d.y !== null
            });
            return factory;
        },

        render: function (args) {

            args = args || {};

            var graph = this.graph;

            var series = args.series || graph.series;
            var vis = args.vis || graph.vis;

            var dotSize = this.dotSize;

            vis.selectAll('*').remove();

            var data = series
                .filter(function (s) {
                    return !s.disabled
                })
                .map(function (s) {
                    return s.stack
                });

            var nodes = vis.selectAll("path")
                .data(data)
                .enter().append("svg:path")
                .attr("d", this.seriesPathFactory());

            var i = 0;
            series.forEach(function (series) {
                if (series.disabled) return;
                series.path = nodes[0][i++];
                this._styleSeries(series);
            }, this);

            series.forEach(function (series) {

                if (series.disabled) return;

                var nodes = vis.selectAll("x")
                    .data(series.stack.filter(function (d) {
                        return d.y !== null
                    }))
                    .enter().append("svg:circle")
                    .attr("cx", function (d) {
                        return graph.x(d.x)
                    })
                    .attr("cy", function (d) {
                        return graph.y(d.y)
                    })
                    .attr("r", function (d) {
                        return ("r" in d) ? d.r : dotSize
                    });

                Array.prototype.forEach.call(nodes[0], function (n) {
                    if (!n) return;
                    n.setAttribute('data-color', series.color);
                    n.setAttribute('fill', 'white');
                    n.setAttribute('stroke', series.color);
                    n.setAttribute('stroke-width', this.strokeWidth);

                }.bind(this));

            }, this);
        }
    });

    Rickshaw.namespace('Rickshaw.Graph.Smoother');

    Rickshaw.Graph.Smoother = Rickshaw.Class.create({

        initialize: function (args) {

            this.graph = args.graph;
            this.element = args.element;
            this.aggregationScale = 1;

            this.build();

            this.graph.stackData.hooks.data.push({
                name: 'smoother',
                orderPosition: 50,
                f: this.transformer.bind(this)
            });
        },

        build: function () {

            var self = this;
            var $ = jQuery;

            if (this.element) {
                $(function () {
                    $(self.element).slider({
                        min: 1,
                        max: 100,
                        slide: function (event, ui) {
                            self.setScale(ui.value);
                            self.graph.update();
                        }
                    });
                });
            }
        },

        setScale: function (scale) {

            if (scale < 1) {
                throw "scale out of range: " + scale;
            }

            this.aggregationScale = scale;
            this.graph.update();
        },

        transformer: function (data) {

            if (this.aggregationScale == 1) return data;

            var aggregatedData = [];

            data.forEach(function (seriesData) {

                var aggregatedSeriesData = [];

                while (seriesData.length) {

                    var avgX = 0, avgY = 0;
                    var slice = seriesData.splice(0, this.aggregationScale);

                    slice.forEach(function (d) {
                        avgX += d.x / slice.length;
                        avgY += d.y / slice.length;
                    });

                    aggregatedSeriesData.push({x: avgX, y: avgY});
                }

                aggregatedData.push(aggregatedSeriesData);

            }.bind(this));

            return aggregatedData;
        }
    });

    Rickshaw.namespace('Rickshaw.Graph.Socketio');

    Rickshaw.Graph.Socketio = Rickshaw.Class.create(Rickshaw.Graph.Ajax, {
        request: function () {
            var socket = io.connect(this.dataURL);
            var self = this;
            socket.on('rickshaw', function (data) {
                self.success(data);
            });
        }
    });
    Rickshaw.namespace('Rickshaw.Series');

    Rickshaw.Series = Rickshaw.Class.create(Array, {

        initialize: function (data, palette, options) {

            options = options || {};

            this.palette = new Rickshaw.Color.Palette(palette);

            this.timeBase = typeof(options.timeBase) === 'undefined' ?
                Math.floor(new Date().getTime() / 1000) :
                options.timeBase;

            var timeInterval = typeof(options.timeInterval) == 'undefined' ?
                1000 :
                options.timeInterval;

            this.setTimeInterval(timeInterval);

            if (data && (typeof(data) == "object") && Array.isArray(data)) {
                data.forEach(function (item) {
                    this.addItem(item)
                }, this);
            }
        },

        addItem: function (item) {

            if (typeof(item.name) === 'undefined') {
                throw('addItem() needs a name');
            }

            item.color = (item.color || this.palette.color(item.name));
            item.data = (item.data || []);

            // backfill, if necessary
            if ((item.data.length === 0) && this.length && (this.getIndex() > 0)) {
                this[0].data.forEach(function (plot) {
                    item.data.push({x: plot.x, y: 0});
                });
            } else if (item.data.length === 0) {
                item.data.push({x: this.timeBase - (this.timeInterval || 0), y: 0});
            }

            this.push(item);

            if (this.legend) {
                this.legend.addLine(this.itemByName(item.name));
            }
        },

        addData: function (data, x) {

            var index = this.getIndex();

            Rickshaw.keys(data).forEach(function (name) {
                if (!this.itemByName(name)) {
                    this.addItem({name: name});
                }
            }, this);

            this.forEach(function (item) {
                item.data.push({
                    x: x || (index * this.timeInterval || 1) + this.timeBase,
                    y: (data[item.name] || 0)
                });
            }, this);
        },

        getIndex: function () {
            return (this[0] && this[0].data && this[0].data.length) ? this[0].data.length : 0;
        },

        itemByName: function (name) {

            for (var i = 0; i < this.length; i++) {
                if (this[i].name == name)
                    return this[i];
            }
        },

        setTimeInterval: function (iv) {
            this.timeInterval = iv / 1000;
        },

        setTimeBase: function (t) {
            this.timeBase = t;
        },

        dump: function () {

            var data = {
                timeBase: this.timeBase,
                timeInterval: this.timeInterval,
                items: []
            };

            this.forEach(function (item) {

                var newItem = {
                    color: item.color,
                    name: item.name,
                    data: []
                };

                item.data.forEach(function (plot) {
                    newItem.data.push({x: plot.x, y: plot.y});
                });

                data.items.push(newItem);
            });

            return data;
        },

        load: function (data) {

            if (data.timeInterval) {
                this.timeInterval = data.timeInterval;
            }

            if (data.timeBase) {
                this.timeBase = data.timeBase;
            }

            if (data.items) {
                data.items.forEach(function (item) {
                    this.push(item);
                    if (this.legend) {
                        this.legend.addLine(this.itemByName(item.name));
                    }

                }, this);
            }
        }
    });

    Rickshaw.Series.zeroFill = function (series) {
        Rickshaw.Series.fill(series, 0);
    };

    Rickshaw.Series.fill = function (series, fill) {

        var x;
        var i = 0;

        var data = series.map(function (s) {
            return s.data
        });

        while (i < Math.max.apply(null, data.map(function (d) {
            return d.length
        }))) {

            x = Math.min.apply(null,
                data
                    .filter(function (d) {
                        return d[i]
                    })
                    .map(function (d) {
                        return d[i].x
                    })
            );

            data.forEach(function (d) {
                if (!d[i] || d[i].x != x) {
                    d.splice(i, 0, {x: x, y: fill});
                }
            });

            i++;
        }
    };

    Rickshaw.namespace('Rickshaw.Series.FixedDuration');

    Rickshaw.Series.FixedDuration = Rickshaw.Class.create(Rickshaw.Series, {

        initialize: function (data, palette, options) {

            options = options || {};

            if (typeof(options.timeInterval) === 'undefined') {
                throw new Error('FixedDuration series requires timeInterval');
            }

            if (typeof(options.maxDataPoints) === 'undefined') {
                throw new Error('FixedDuration series requires maxDataPoints');
            }

            this.palette = new Rickshaw.Color.Palette(palette);
            this.timeBase = typeof(options.timeBase) === 'undefined' ? Math.floor(new Date().getTime() / 1000) : options.timeBase;
            this.setTimeInterval(options.timeInterval);

            if (this[0] && this[0].data && this[0].data.length) {
                this.currentSize = this[0].data.length;
                this.currentIndex = this[0].data.length;
            } else {
                this.currentSize = 0;
                this.currentIndex = 0;
            }

            this.maxDataPoints = options.maxDataPoints;


            if (data && (typeof(data) == "object") && Array.isArray(data)) {
                data.forEach(function (item) {
                    this.addItem(item)
                }, this);
                this.currentSize += 1;
                this.currentIndex += 1;
            }

            // reset timeBase for zero-filled values if needed
            this.timeBase -= (this.maxDataPoints - this.currentSize) * this.timeInterval;

            // zero-fill up to maxDataPoints size if we don't have that much data yet
            if ((typeof(this.maxDataPoints) !== 'undefined') && (this.currentSize < this.maxDataPoints)) {
                for (var i = this.maxDataPoints - this.currentSize - 1; i > 1; i--) {
                    this.currentSize += 1;
                    this.currentIndex += 1;
                    this.forEach(function (item) {
                        item.data.unshift({x: ((i - 1) * this.timeInterval || 1) + this.timeBase, y: 0, i: i});
                    }, this);
                }
            }
        },

        addData: function ($super, data, x) {

            $super(data, x);

            this.currentSize += 1;
            this.currentIndex += 1;

            if (this.maxDataPoints !== undefined) {
                while (this.currentSize > this.maxDataPoints) {
                    this.dropData();
                }
            }
        },

        dropData: function () {

            this.forEach(function (item) {
                item.data.splice(0, 1);
            });

            this.currentSize -= 1;
        },

        getIndex: function () {
            return this.currentIndex;
        }
    });

    return Rickshaw;
}));
